概念
创建线程池时,有两个重要参数
- corePoolSize
核心线程数,表示线程池中的常驻线程的个数 - maximumPoolSize
最大线程数,表示线程池中能开辟的最大线程个数
对线程池负责执行的任务分为三种情况:
- CPU密集型任务
- IO密集型任务
- 混合型任务
Java可以通过以下API拿到你电脑的核心数:
Runtime.getRuntime().availableProcessors();
CPU密集型任务
CPU密集型任务的特点:线程在执行任务时会一直利用CPU,比如找出1-1000000中的素数。所以对于这种情况,就尽可能避免发生线程上下文切换。
比如,电脑只有一个CPU,如果有两个线程在同时执行找素数的任务,那么这个CPU就需要额外的进行线程上下文切换,从而达到线程并行的效果,此时执行这两个任务的总时间为:
任务执行时间*2+线程上下文切换的时间
而如果只有一个线程,这个线程来执行两个任务,那么时间为:
任务执行时间*2
所以对于CPU密集型任务,线程数最好就等于CPU核心数。
只不过,为了应对线程执行过程发生缺页中断或其他异常导致线程阻塞的请求,我们可以额外在多设置一个线程,这样当某个线程暂时不需要CPU时,可以有替补线程来继续利用CPU。
结论
对于CPU密集型任务,可以设置线程数为:CPU核心数+1
IO密集型任务
线程在执行IO型任务时,可能大部分时间都阻塞在IO上。
假如现在有10个CPU,如果我们只设置了10个线程来执行IO型任务,那么很有可能这10个线程都阻塞在了IO上,这样这10个CPU就都没活干了。
结论
对于IO型任务,我们通常会设置线程数为:2*CPU核心数。
题外
就算是设置为了2*CPU核心数
,一定是最佳的吗?
比如,有10个CPU,设置线程数为20,那么也有可能这20个线程同时阻塞在了IO上,所以可以再增加线程,从而去压榨CPU的利用率。
通常,如果IO型任务执行的时间越长,那么同时阻塞在IO上的线程就可能越多,我们就可以设置更多的线程。
线程肯定不是越多越好,我们可以通过以下这个公式来进行计算:
线程数 = CPU核心数 *( 1 + 线程等待时间 / 线程运行总时间 )
线程等待时间:指的就是线程没有使用CPU的时间,比如阻塞在了IO
线程运行总时间:指的是线程执行完某个任务的总时间
可以利用 抽样来估计这两个时间:
图中表示,在刚刚这次抽样过程中,run()总共的执行时间为255118ms,利用了CPU的时间为1017ms,所以没有利用CPU的时间为255118ms-1017ms。
我们可以计算出:
线程等待时间 =255118ms-1017ms
线程运行总时间 = 255118ms
线程数 = 12*(1+(255118ms-1017ms)/255118ms) = 23.95
所以根据公式算出来的线程为23、24个线程左右。
总结
-
CPU密集型任务
CPU核心数+1,这样既能充分利用CPU,也不至于有太多的上下文切换成本 -
IO型任务
建议压测,或者先用公式计算出一个理论值(理论值通常都比较小) -
对于核心业务(访问频率高)
可以把核心线程数设置为我们压测出来的结果,最大线程数可以等于核心线程数,或者大一点点,比如我们压测时可能会发现500个线程最佳,但是600个线程时也还行,此时600就可以为最大线程数。 -
对于非核心业务(访问频率不高)
核心线程数可以比较小,避免操作系统去维护不必要的线程,最大线程数可以设置为我们计算或压测出来的结果。
实际工作中,一定要根据压测结果进行测试,动态调整,没有最佳,只有更佳。