ThreadPoolExecutor的任务执行机制
我们在做异步操作的时候常用的线程池有jdk的ThreadPoolExecutor 和spring的ThreadPoolTaskExecutor,ThreadPoolTaskExecutor其实也是对ThreadPoolExecutor进行了一层封装,所以我们看一下ThreadPoolExecutor到底值怎样执行任务的。
- 背景
最近在使用线程池的时候遇到了两个奇怪的问题
首先看代码
我的线程池配置示例是这样的:
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(3);
executor.setMaxPoolSize(4);
executor.setQueueCapacity(10);
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("demoAysn-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
异步执行的任务睡眠5秒
private static class ExecutorTestThread extends Thread{
private int count;
public ExecutorTestThread(int count){
this.count = count;
}
@Override
public void run(){
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(new SimpleDateFormat("HH:mm:ss").format(new Date()) + ": ExecutorTestThread run ,taskIndex = " + count);
}
}
问题1:猜想下,如果我循环提交20个任务,任务会怎样执行?(此处为了保证先开始的任务先结束,使打印出的结果更直观,每个任务提交间隔了10ms)
我起初认为会是 同一时间执行4个(maxPoolSize), 执行5批。
但实际运行结果如下图:
前两批都是5个任务同时执行,接着是两批4,最后一批2个任务执行,
除此之外任务编号也需要关注一下,任务不是按顺序执行的。
然后,在keepAlive的时间内,我再执行20个任务,执行的编号又是不同的,结果如下:
问题2:将queueCapacity 改为1,重复上述操作,运行结果如下两张图(这部分不需要再关注执行顺序,所以把任务提交的间隔去掉了)
可以看出来运行的结果看起来变得混乱,有时一批多有时一批少,变得没有规则。
接下来带着这两个问题我们好好了解一下 ThreadPoolTaskExecutor 。
- 源码分析
ThreadPoolTaskExecutor 或者ThreadPoolExecutor的submit方法内部调用的都是ThreadPoolExecutor的execute方法,下边是ThreadPoolExecutor.execute(Runable command)方法的源码:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
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);
}
稍作解释大家就会发现这部分代码非常简洁清晰,很直接地告诉我们它是如何安排任务的。
- 成员变量ctl类型为 AtomicInteger, 它用32位的int类型来存放线程池状态和正在运行的线程数(注意 不是正在执行的任务数,而是线程池自己的可以用来执行任务的线程数,起始为0,最大为maxPoolSize,一旦达到过corePoolSize,则不会再少于corePoolSize),其对应的二进制数的前三位代表状态,其余代表线程数。
- workerCountOf(c) 方法 即获取线程池的运行的线程数
- addWorker(command,true)方法是为线程池增加线程,并执行任务 ,它有两个参数,第一个即被提交的需要执行的任务,第二个布尔类型的参数标识当前是在添加核心线程(最大数量corePoolSize),还是再添加额外扩充的线程(maxPoolSize-corePoolSize部分)。若添加成功则执行对应任务,并返回true,否则返回false。
- is(Running(c)) 判断当前线程池是否是Running状态(正常我们不关闭线程池就都是true),
- reject(commond) 拒绝任务,并按照拒绝任务策略执行对应的内容
以上足够我们清楚了解它处理线程任务的流程了,总体流程图如下
可概括为:
(1)优先尝试扩展线程池的工作线程至corePoolSize,
(2)再进行尝试进行入队列,
(3)最后再尝试扩展线程池的工作线程数
(4)最终失败则执行拒绝策略
注意点:
步骤(1)经常是在线程池的初始化后的初始使用阶段会执行,只要线程数达到过corePoolSize,后边任务提交进来都是直接进行了入队列操作(即workCountOf(c)始终大于corePoolSize)
- 问题解答
问题1.1:为什么同时会有5个线程执行任务,比maxPoolSize还要多一个?
答:因为拒绝策略为 ThreadPoolExecutor.CallerRunsPolicy
它的处理方式为
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
即拒绝时,则会在主线程执行该任务,所以会多处一个来,因为主线程在执行该任务,所以会阻塞后边任务的提交,直至主线程任务处理结束。
问题1.2:为什么第一次执行的顺序是 1、2、3、14、15、4、5、6、7、20……
答:因为第一次线程池未达到corePoolSize所以先提交三个进coreThread(3),后边的4-13进入队列等待空闲线程,队列满了之后14到来则需要扩展线程数量到maxPoolSize(4),15到来则进行了CallerRunsPolicy拒绝策略,在主线程执行。
这一批结束后,4-7出队列开始执行,16-19进队列,20主线程执行,此时所有任务都提交完毕,剩余的依次在线程池中处理,不再占用主线程。
问题1.3:为什么第二次执行顺序又变了
答:因为线程池的运行线程数已至少达到maxPoolSize,直接进行后边的入队列操作和拒绝策略。
问题2:queueCapacity 改为1,为什么同时运行的线程数会不定,甚至会小于corePoolSize
答:也是因为线程池的运行线程数已至少达到maxPoolSize,直接进行后边的入队列操作和拒绝策略,而且因为队列容量太小,仅为1,而且循环提交任务时没有时间间隔,可能会出现出队列的速度不如入队列速度,所以即使线程池没有执行任务,但队列已满,就执行了拒绝策略。
- 总结
1、因为ThreadPoolExecutor内置的CallerRunsPolicy拒绝策略会在队列满的情况下使用主线程执行任务,所以在处理多个请求或者事件监听时,同时执行任务的数据可能会达到maxPoolSize + n 个。
2、CallerRunsPolicy 和 maxPoolSize>corePoolSize 可能会使任务的submit顺序与执行顺序不一致,如果有必要使其一致,可用固定大小的线程池和大容量的队列,且使用其他策略不会改变执行顺序的策略(自定义策略更灵活)。
3、注意CallerRunsPolicy可能会使任务在主线程执行,使得队列虽然有空间,但在任务执行结束前无法将后续的任务提交进线程池,如果可以预测执行时间,可自定义拒绝策略避免阻塞后续任务提交。
4、队列容量的设置需考虑占用内存、异常导致任务丢失等情况,设置合理的值,太小的话提交过快可能无法充分利用线程池,一般需大于maxPoolSize。