一、线程池是每个程序员要掌握的一项技能,我们要如何去理解线程池呢,可以看下线程池是如何创建的。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
}
ThreadPoolExecutor 的构造函数中,有几个很重要的参数。
- corePoolSize: 核心线程数量最大值
- maximumPoolSize: 最大线程数量
- keepAliveTime: 非核心线程存活时间
- unit: keepAliveTime时间单位
- workQueue:在任务执行之前用于保存任务的队列
- threadFactory: 线程工厂可用于创建新线程
- handler: 当达到线程界限和队列容量线程池的拒绝策略
线程池的工作原理如下图所示:
- 当线程池小于 corePoolSize 时,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程。
- 当线程池达到 corePoolSize 时,新提交任务将被放入 workQueue 中,等待线程池中任务调度执行
- 当 workQueue 已满,且 maximumPoolSize > corePoolSize时,新提交任务会创建新线程执行任务
- 当提交任务数超过 maximumPoolSize 时,新提交任务由 RejectedExecutionHandler 处理
- 当线程池中超过 corePoolSize 线程,空闲时间达到 keepAliveTime 时,关闭空闲线程
- 当设置 allowCoreThreadTimeOut(true) 时,线程池中 corePoolSize 线程空闲时间达到 keepAliveTime 也将关闭
二、如何去设置线程池的大小呢?
阿里巴巴规范里约定不允许使用Executors创建自带的线程池,而是通过ThreadPoolExecutor方式来自定义线程池。
如果线程池过大,那么大量的线程将在相对很少的CPU和内存资源上发生竞争,会到值更高的内存使用量,并且还可能耗尽资源。如果线程池过小,那么将会导致许多空闲的处理器无法执行工作,从而降低吞吐率。
在Java并发编程实战这本书中,作者给出了以下公式:
N(threads) = N(cpu) * U(cpu) * (1 + W / C)
N(cpu) = CPU 核数
U(cpu) = CPU 利用率 0 <= U(cpu) <= 1
W/C = CPU 等待时间与计算时间比率
其中N(cpu) 可根据Runtime.getRuntime().availableProcessors() 获得。
线程池中任务可大致分为计算密集型以及IO密集型。
一、 对于计算密集型的任务,CPU闲置时间会很短,CPU利用率趋于100%
N(threads) ≈ N(cpu) * 1 * (1 + 0) = N(cpu)
但是当计算机密集型的线程偶尔由于页缺失故障或者其他原因暂停时,会导致CPU的时钟周期浪费,我们可以增加一个"额外线程"。
对于计算密集型的任务,可大致设为: N(threads) = N(cpu) + 1
二、 对于IO密集型的任务,阻塞的耗时会是计算密集型的很多倍。
N(threads) ≈ N(cpu) * 1 * (1 + 1 ) = 2N(cpu)
因此IO密集型的任务,可大致设为: N(threads) = 2N(cpu)
以上仅仅是两种任务的一种估算,这种估算不是很精确,还是需要通过一些分析和监控工具获得。