原创

(七)Java线程池的大小如何确定?

温馨提示:
本文最后更新于 2021年08月05日,已超过 371 天没有更新。若文章内的图片失效(无法正常加载),请留言反馈或直接联系我

线程的使用目的是提高运行速度,提高运行的速度是要充分使用CPU和I/O 的利用率。

这就涉及到CPU密集型程序和I/O密集型程序的区别了。

CPU 密集型

CPU密集型也叫计算密集型,指的是系统的硬盘、内存性能相对CPU要好很多,此时,系统运作大部分的状况是CPU Loading 100%,CPU要读/写I/O(硬盘/内存),I/O在很短的时间就可以完成,而CPU还有许多运算要处理,CPU Loading很高。

比如说要计算1+2+3+.....+ 1亿、计算圆周率后几十位、数据分析。
都是属于CPU密集型程序。

此类程序运行的过程中,CPU占用率一般都很高。

假如在单核CPU情况下,线程池有6个线程,但是由于是单核CPU,所以同一时间只能运行一个线程,考虑到线程之间还有上下文切换的时间消耗,还不如单个线程执行高效。

所以!!!单核CPU处理CPU密集型程序,就不要使用多线程了。

假如是6个核心的CPU,理论上运行速度可以提升6倍。每个线程都有 CPU 来运行,并不会发生等待 CPU 时间片的情况,也没有线程切换的开销。

所以!!!多核CPU处理CPU密集型程序才合适,而且中间可能没有线程的上下文切换(一个核心处理一个线程)。

简单的说,就是需要CPU疯狂的计算。

IO密集型

IO密集型指的是系统的CPU性能相对硬盘、内存要好很多,此时,系统运作,大部分的状况是CPU在等I/O (硬盘/内存) 的读/写操作,但CPU的使用率不高。

所以用脚本语言像python去做I/O密集型操作,效率就很快。

简单的说,就是需要大量的输入输出,不如读文件、写文件、传输文件、网络请求。

区别和使用:

IO密集型:大量网络,文件操作
CPU 密集型:大量计算,cpu 占用越接近 100%, 耗费多个核或多台机器

业务要具体分析,假如CPU现在是10%,数据量增大一点点,CPU狂飙,那也可能CPU密集型。

如何确定线程池大小?

目前确定线程池大小一共有两派,对应着两个不同的公式,但是对错都没有人证明,比较不可控的因素多,具体还是要看自己的业务。

第一派:《java并发编程实践》

线程数不是越多越好。

由于CPU的核心数有限,线程之间切换也需要开销,频繁的切换上下文会使性能降低,适得其反。

简单的总结就是:

Ncpu 表示 核心数。

 如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 Ncpu+1

 如果是IO密集型任务,参考值可以设置为 2 N*cpu

上面两个公式为什么是Ncpu+1 呢,而不是Ncpu+2 呢,为什么不是3 **Ncpu* 呢?

解释:

在《Java并发编程实践》中,是这样来计算线程池的线程数目的:

简单解释:

一个基准负载下,使用 几种不同大小的线程池运行你的应用程序,并观察CPU利用率的水平。
给定下列定义:

Ncpu = CPU的数量
Ucpu = 目标CPU的使用率, 0 <= Ucpu <= 1
W/C = 等待时间与计算时间的比率

为保持处理器达到期望的使用率,最优的池的大小等于:

 Nthreads = Ncpu x Ucpu x (1 + W/C)

CPU数量是确定的,CPU使用率是目标值也是确定的,W/C也是可以通过基准程序测试得出的。

疑问一:对于计算密集型应用,假定等待时间趋近于0,是的CPU利用率达到100%,那么线程数就是CPU核心数,那这个+1意义何在呢?

根据 Nthreads = Ncpu x Ucpu x (1 + W/C),等待时间趋近于0 即 W/C = 0,结果就是 Ucpu ,那为什么要+1 呢?

《Java并发编程实践》这么说:

计算密集型的线程恰好在某时因为发生一个页错误或者因其他原因而暂停,刚好有一个“额外”的线程,可以确保在这种情况下CPU周期不会中断工作。

所以 Ncpu+1 是一个经验值。

对于IO密集型应用,假定所有的操作时间几乎都是IO操作耗时,那么 W/C的值就为1,Ucpu 要达到100%利用率。

根据 Nthreads = Ncpu x Ucpu x (1 + W/C),那么对应的线程数确实为 2Ncpu

对于包含I/O操作或者其他阻塞的任务,由于线程不会一直执行,因此线程池的数量应该更多。

在《linux多线程服务器端编程》中有一个思路,CPU计算和IO的阻抗匹配原则

如果线程池中的线程在执行任务时,密集计算所占的时间比重为P(0<P<=1),而系统一共有C个CPU,为了让CPU跑满而又不过载,线程池的大小经验公式 T = C / P。在此,T只是一个参考,考虑到P的估计并不是很准确,T的最佳估值可以上下浮动50%。

这个经验公式的原理很简单,T个线程,每个线程占用P的CPU时间,如果刚好占满C个CPU,那么必有 T * P = C。

疑问二:如何在代码中确定CPU数量?

Java代码中可以通过Rumtime来获得CUP的数目:

int N_CPUS = Runtime.getRuntime().availableProcessor();

第二派:《Java 虚拟机并发编程》

重点概括就是:

线程数 = CPU可用核心数/(1 - 阻塞系数),其中阻塞系数的取值在0和1之间。
计算密集型任务的阻塞系数为0,而IO密集型任务的阻塞系数则接近1。一个完全阻塞的任务是注定要挂掉的,所以我们无须担心阻塞系数会达到1。

这里说到的 IO密集型任务的阻塞系数则接近1, 也验证了上面第一派中 Nthreads = Ncpu x Ucpu x (1 + W/C),那么对应的线程数确实为 2Ncpu

对于派系一,假设cpu100%运转,即撇开CPU使用率这个因素,线程数=Ncpu*(1+w/c)。

现在假设将派系二的公式等于派系一公式,即令

Ncpu /(1-阻塞系数)= Ncpu*(1+w/c)

化简得出: 阻塞系数=w/(w+c)

即 阻塞系数=阻塞时间/(阻塞时间+计算时间),这个结论在派系二后续中得到应征,如下图:

由此可见,派系一和派系二其实是一个公式。

解释:

这一派的公式就为:

最佳线程数目 = CPU数目/(1-阻塞系数)

其中 ,

阻塞系数 = w/(w+c)
即:
阻塞系数 = 阻塞时间/(阻塞时间+计算时间)

其中 w/(w+c) 可以根据算出来的,w是线程等待时间,即IO时间,c 是线程CPU时间。

这样就可以计算CPU和IO混合操作的任务线程数量了,见下面这个疑问。

疑问三:如果一个web程序有CPU操作,也有IO操作,那该如何设置呢?

如果可以拆分任务,那么就是给CPU的计算型任务分配 N+1 个线程,给IO密集型的任务分配 2N 个线程。

如果不能拆分,就需要估算了:

根据上面的公式:

最佳线程数目 = CPU数目/(1-阻塞系数) = CPU数目/(1- w/(w+c))

把w、c 换成 线程等待时间、线程CPU时间

化简后为:

最佳线程数目 = ((线程等待时间 + 线程CPU时间)/线程CPU时间 )* CPU数目

这个公式进一步转化为:

最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1)* CPU数目

以上可以得出一个结论:

线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。

疑问四:是否使用线程池就一定比使用单线程高效呢?

答案是否定的,比如Redis就是单线程的,但它却非常高效,基本操作都能达到十万量级/s。从线程这个角度来看,部分原因在于:

  • 多线程带来线程上下文切换开销,单线程就没有这种开销


下面据说是个腾讯的面试题:

问题一:

假如一个程序平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么最佳的线程数应该是?

根据上面这个公式估算得到最佳的线程数:((0.5+1.5)/0.5)*8=32。

问题二:

假如在一个请求中,计算操作需要5ms,DB操作需要100ms,对于一台8个CPU的服务器,总共耗时100+5=105ms,而其中只有5ms是用于计算操作的,CPU利用率为5/(100+5)。使用线程池是为了尽量提高CPU的利用率,减少对CPU资源的浪费,假设以100%的CPU利用率来说,要达到100%的CPU利用率,又应该设置多少个线程呢?

((5+100)/5)*8=168 个线程。

问题三:

那如果现在这个IO操作是DB操作,而DB的QPS上限是1000,这个线程池又该设置为多大呢?

这个可以安装比例进行,根据上面算出168最大的线程数,可以反推出DB的最大QPS:

168*(1000/(100+5))=1600

如果现在DB的QPS最大为1000,那么对应的,最大只能设置168*(1000/1600)=105个线程


总结

即使有上面的简单估算方法,也许看似合理,但实际上也未必合理,都需要结合系统真实情况(比如是IO密集型或者是CPU密集型或者是纯内存操作)和硬件环境(CPU、内存、硬盘读写速度、网络状况等)来不断尝试达到一个符合实际的合理估算值。

如果任务可以分离:

 如果是CPU密集型任务,参考值可以设为 Ncpu+1

 如果是IO密集型任务,参考值可以设置为 2 N*cpu

如果任务不可以分离:

参考线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目

另外:

在调试线程数数量的时候,可以使用jstack 进行测试,如果发现线程都处于waiting(等待状态获取任务),那么说明线程数是够用的;如果都是running,则可以继续调高线程数量。


参考:

  1. 什么是CPU密集型、IO密集型?:https://blog.csdn.net/youanyyou/article/details/78990156
  2. 《java虚拟机并发编程》
  3. 腾讯面试官:线程池要设置多大:http://www.zyiz.net/tech/detail-121726.html
  4. 如何合理地估算线程池大小?:http://ifeve.com/how-to-calculate-threadpool-size/
正文到此结束
关注公众号 【HelloCoder】
免费领取Java学习资料
让技术,化繁为简
本文目录