ThreadPoolExecutor的任务执行机制

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);
}

稍作解释大家就会发现这部分代码非常简洁清晰,很直接地告诉我们它是如何安排任务的。

  1. 成员变量ctl类型为 AtomicInteger, 它用32位的int类型来存放线程池状态和正在运行的线程数(注意 不是正在执行的任务数,而是线程池自己的可以用来执行任务的线程数,起始为0,最大为maxPoolSize,一旦达到过corePoolSize,则不会再少于corePoolSize),其对应的二进制数的前三位代表状态,其余代表线程数。
  2. workerCountOf(c) 方法 即获取线程池的运行的线程数
  3. addWorker(command,true)方法是为线程池增加线程,并执行任务 ,它有两个参数,第一个即被提交的需要执行的任务,第二个布尔类型的参数标识当前是在添加核心线程(最大数量corePoolSize),还是再添加额外扩充的线程(maxPoolSize-corePoolSize部分)。若添加成功则执行对应任务,并返回true,否则返回false。
  4. is(Running(c)) 判断当前线程池是否是Running状态(正常我们不关闭线程池就都是true),
  5. 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。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值