含泪播种的人必定能含笑收获。
有个朋友Hunter跟我聊,最近他参加腾讯的面试,在二面的时候被问到了关于线程池线程数目设置的一个问题。此处记录下这个问题的面试过程,以及后面关于此问题的理论方面的知识讲解。面试
面试官开场了:算法
线程池你用过吧,线程数是怎么设置的呢?
Hunter心想,这不难啊,曾经在《Java并发编程》一书中有看到过线程池中线程数目设置的讲述,因而张口就来:数据库
线程数的设置须要考虑三方面的因素,服务器的配置、服务器资源的预算和任务自身的特性。具体来讲就是服务器有多少个CPU,多少内存,IO支持的最大QPS是多少,任务主要执行的是计算、IO仍是一些混合操做,任务中是否包含数据库链接等的稀缺资源。线程池的线程数设置主要取决于这些因素。
面试官追问来了:编程
那具体是怎么设置呢?
Hunter略一思忖,整理了下思路,娓娓道来:服务器
假设机器有N个CPU,那么对于计算密集型的任务,应该设置线程数为N+1;对于IO密集型的任务,应该设置线程数为2N;对于同时有计算工做和IO工做的任务,应该考虑使用两个线程池,一个处理计算任务,一个处理IO任务,分别对两个线程池按照计算密集型和IO密集型来设置线程数。
面试官表情毫无变化,接着发问:网络
N+1和2N是怎么来的?
Hunter张口就来:并发
是个经验值。
面试官:学习
经验值吗?那为何不是N+2或者N+3,而非得是N+1呢?
Hunter被驳得稍有点懵,脑子里努力在回想学习过的那些技术点,竟一时语塞。测试
看得出来面试官略有不满,因而提示道:spa
那假如在一个请求中,计算操做须要5ms,DB操做须要100ms,对于一台8个CPU的服务器,怎么设置线程数呢?
Hunter努力平复心情,紧接着最开始的思路,说到:
这是一个计算和IO混合型的任务,能够将其分解为两个线程池来处理。一个线程池处理计算操做,设置N+1=9个线程,一个线程处理IO操做,设置2N=16个线程。
面试官:
若是一个任务同时包含了一个计算操做和DB操做呢,不能拆分怎么设置?你能讲一下具体的计算过程吗?
Hunter略有点慌,内心不断给本身暗示:这个问题不难不难。而后不断回想看过的《Java并发编程实战》和《Java虚拟机并发编程》中关于线程池设置的章节,并试图将本身对这个问题的分析思路也表达出来。
首先这个任务总体上是一个IO密集型的任务。在处理一个请求的过程当中,总共耗时100+5=105ms,而其中只有5ms是用于计算操做的,CPU利用率为5/(100+5)。使用线程池是为了尽可能提升CPU的利用率,减小对CPU资源的浪费,假设以100%的CPU利用率来讲,要达到100%的CPU利用率,对于一个CPU就要设置其利用率的倒数个数的线程数,也即1/(5/(100+5)),8个CPU的话就乘以8。那么算下来的话,就是……168,对,这个线程池要设置168个线程数。
面试官表情略有缓和,嘴角微微一笑:
若是实际的任务差别较大,不一样任务实际的CPU操做耗时和IO操做耗时有所不一样,那么怎么设置线程数呢?
通过刚才的分析过程,Hunter内心已经回忆起了这块的知识点,已然不慌了。
那对全部任务的CPU操做耗时和IO操做耗时求个平均值就行了。
Hunter内心渐渐恢复了自信,大脑的利用率瞬间提升好几十个百分点。
面试官轻轻“嗯”了一声,表示承认。
那若是如今这个IO操做是DB操做,而DB的QPS上限是1000,这个线程池又该设置为多大呢?
通过刚才的心理调整,对问题完整的分析过程,以及面试官的略微承认,Hunter已经知道如何去更好地回答面试官的问题了。
按比例来减小就能够了,按照以前的计算过程,能够计算出来当线程数设置为168的时候,DB操做的QPS为,168 (1000/(100+5))=1600,若是如今DB的QPS最大为1000,那么对应的,最大只能设置168(1000/1600)=105个线程。
面试官此次是真的满意了,给这个回答给了一个正面的评价:
思路挺清晰的。那设置线程池的时候除了考虑这些,还须要考虑哪些内容呢?
Hunter此时已经彻底找回自信了,不惧任何问题。
除了考虑任务CPU操做耗时、IO操做耗时以外,还须要服务器的内存资源、硬盘资源、网络带宽等等的。
面试官点点头,看起来Hunter已经得到了面试官的正式承认了。面试官告诉Hunter,表现不错,等接下来的面试安排吧。
Hunter心里异常激动,这真算是一次“死里逃生”的经历了。面试结束后,Hunter压抑兴奋,立刻去找到《Java并发编程实战》和《Java虚拟机并发编程》两本书,翻到对应的章节,想确认下本身的回答。
果真,压力除了会形成紧张以外,也能提升大脑利用率。Hunter在调整状态后的回答彻底正确。附上两本书中对线程池设置的理论。
在《Java并发编程实践》中,是这样来计算线程池的线程数目的:
在一个基准负载下,使用 几种不一样大小的线程池运行你的应用程序,并观察CPU利用率的水平。
给定下列定义:Ncpu = CPU的数量 Ucpu = 目标CPU的使用率, 0 <= Ucpu <= 1 W/C = 等待时间与计算时间的比率为保持处理器达到指望的使用率,最优的池的大小等于:
Nthreads = Ncpu x Ucpu x (1 + W/C)
这种计算方式,咱们须要知道上面定义的几个数值,才能计算出来线程池须要设置的线程数。其中,CPU数量是肯定的,CPU使用率是目标值也是肯定的,W/C也是能够经过基准程序测试得出的。
而在《Java虚拟机并发编程》中,则是这样来计算线程池的线程数目的:
线程数 = CPU可用核心数/(1 - 阻塞系数),其中阻塞系数的取值在0和1之间。
计算密集型任务的阻塞系数为0,而IO密集型任务的阻塞系数则接近1。一个彻底阻塞的任务是注定要挂掉的,因此咱们无须担忧阻塞系数会达到1。
这种计算方式,咱们须要知道CPU可用核心数和阻塞系数,才能计算出来线程池须要设置的线程数目。其中,CPU可用核心数是肯定的,阻塞系数能够经过公式:阻塞系数=阻塞时间/(阻塞时间+计算时间),其实也就是上一种算法中的W/C的方式来计算,因此阻塞系数也是能够经过基准程序计算得出的。
那么咱们再来看所谓的N+1与2N的经验值的来源。
计算密集型应用
以第一种计算方式来看,对于计算密集型应用,假定等待时间趋近于0,是的CPU利用率达到100%,那么线程数就是CPU核心数,那这个+1意义何在呢?
《Java并发编程实践》这么说:
计算密集型的线程刚好在某时由于发生一个页错误或者因其余缘由而暂停,恰好有一个“额外”的线程,能够确保在这种状况下CPU周期不会中断工做。
因此N+1确实是一个经验值。
IO密集型应用
一样以第一种方式来看,对于IO密集型应用,假定全部的操做时间几乎都是IO操做耗时,那么W/C的值就为1,那么对应的线程数确实为2N。
本文由微型公众号【Dali王的技术博客】原创,扫码关注获取更多原创技术文章。
![]()