在使用ThreadPoolExecutor
时,我也跟参考文章作者(估计很多人也一样)产生了同样的误解:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
看这个参数很容易让人以为是线程池里保持corePoolSize个线程,如果不够用,就加线程入池直至maximumPoolSize大小,如果还不够就往workQueue里加,如果workQueue也不够就用RejectedExecutionHandler来做拒绝处理。
研究了下其构造函数入参,对于其中 workQueue 和 workThread 是如何增长的有一些疑问:究竟是先创建工作线程处理新任务,还是先放到queue中等待再等workThread空了再处理。就此Google了下,并且探寻了下源码,通过源码发现了门道。
一篇参考文章的说法:
“3)当workQueue放不下新入的任务时,新建线程入池,并处理请求,如果池子大小撑到了maximumPoolSize就用RejectedExecutionHandler来做拒绝处理”
我来补充一下这其中的细节,逻辑解析自 jdk11 ThreadPoolExecutor.excecute():
- 线程池引入新任务时,先判断当前线程数是否 < corePoolSize;是则新建一个core线程专门处理这个新任务;否则下一步;
- 将新任务加入队列,如果能成功加入队列(队列没满)则等待处理;否则尝试新建work线程,如果work线程也加不了(高于maximumPoolSize),那就 reject 这个任务。
所以这就可以解答最初的问题:
(1)当任务少到不及 coreThreadSize 大小或者 coreThreadSize 就能处理得过来时,是用不到 workQueue 的;
(2)当 coreThread 满载时,新进来的任务会先进入 workQueue(并不是直接就创建workThread),加到 queue 后,二次检查:若当前没有 workThread 那就创建一个,否则就暂时完成处理,等待已有线程处理 queue。
(3)如果 workQueue 也满了加不了,那就创建一个 workThread 单独去处理这个 task,如果 workThread 也满了加不了,那就 reject 这个 task!
(4)综上,coreThreadSize 、queueSize、maxThreadSize 这三者要结合起来,根据实际情况权衡大小!如果任务很快就能处理完,并且新任务进来的速度还不如线程处理任务的速度,那大概率 coreThread 就能够应付自如,根本用不到 queue,queueSize 的大小无需配多大的;如果任务生产速度大于几个 coreThread 的消费速度,那必定是要入队的,至于 queueSize 设多大,就要权衡消费跟生产之间的效率差了。比如线程消费速率为 10 task/ min,而 task 生产速率为 60 task/min,1:5 关系,假定 coreThreadSize = 3,那么每分钟有 30 task无法处理,这 30 task 的缺口就需要 queue 或者 maxThreadSize - coreThreadSize 去弥补,比如我可以设定 maxThreadSize = coreThreadSize + 3,那我就多了 3 * 10 = 30 的消费力,正好弥补缺口,那么 queue 不需要设多大,线程也应付地过来(此时 queue 的作用更多是为了在 workThread 创建完毕之前暂存task),但是注意,如果任务是一次性生产完,再等待多线程陆续处理的,那么queueSize就不能低于总的任务数。假设 (maxThreadSize - coreThreadSize)* 10 的消费力仍有缺口,如果不想触发 reject 那 queue 就要考虑设长一些。当然以上计算只是理想状态下的计算,实际场景下,消费力并不是随着 thread 增加而线性增长,跟 CPU 核数和CPU时间片和任务切换等等都有关系。
(5)queueSize 并不是越大越好的,很简单,如果队列无限增长,内存会 OOM~
ThreadPoolExecutor.excecute() 源码:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}