在看《Java并发编程的艺术》线程池这一块时,提到了要合理地配置线程池,要分析任务特性。任务的性质:CPU密集型、IO密集型和混合型。

CPU密集型和IO密集型

CPU密集型也是指计算密集型,大部分时间用来做计算逻辑判断等CPU动作的程序称为CPU密集型任务。该类型的任务需要进行大量的计算,主要消耗CPU资源。这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。

IO密集型任务指任务需要执行大量的IO操作,涉及到网络、磁盘IO操作,对CPU消耗较少。

和线程池配置的关系

CPU密集型任务应配置尽可能小的线程,如配置CPU数目+1个线程的线程池。由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如2*CPU数目。

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

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

如何确定线程池大小?
线程数不是越多越好。

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

简单的总结就是:

Ncpu 表示 核心数。

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

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

上面两个公式为什么是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意义何在呢?

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

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

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

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

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

那么对应的线程数确实为 2Ncpu 。

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

int N_CPUS = Runtime.getRuntime().availableProcessor();
复制
对于包含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。

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

有一个估算公式:

最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目
复制
这个公式进一步转化为:

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