ThreadPoolExecutor逻辑链梳理 - 1 线程池的执行入口

【从问题开始】

在使用线程池的时候往往会看到线程池reject相关日志,例如以下日志:

[Running, pool size = 80, active threads = 3, queued tasks = 3, completed tasks = 1674]

遂产生疑问:为什么线程池大小为80,队列长度为3的线程池,只有3个active threads就会触发reject机制?

目录

线程池任务的入口 - Execute 


ThreadPoolExecutor的核心成员变量

线程池的核心参数包括:

// ctl用于封装状态

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

// 用于保存任务和移交给工作线程的队列(没获得执行资源的队列)

private final BlockingQueue<Runnable> workQueue;

// 包含池中所有工作线程的集合。Worker是一个带锁的执行器。这是执行资源的队列,但也不意味着一定就开始执行了,因为worker本身也是AQS竞争

private final HashSet<Worker> workers = new HashSet<Worker>();

线程池任务的入口 - Execute 

想要搞清楚reject的机制,就要先研究reject的触发策略。先看线程池任务的入口:execute

    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        
        // 流程1. 判断核心线程池是否已满,未满则执行addWorker创建核心线程
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }

        // 流程2. 判断线程池状态是否是RUNNING,阻塞队列是否能塞新的可执行对象
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            // 流程2.1 再判断线程池状态,非运行态,执行拒绝策略
            if (! isRunning(recheck) && remove(command))
                reject(command);
            // 流程2.2 运行态,如果线程池的工作Worker数量为0,创建一个空执行对象,
            // 这里是为了保证线程池至少有东西跑
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        // 流程3. 再次尝试addWorker创建非核心线程
        else if (!addWorker(command, false))
            reject(command);
    }

从这部分可以看出,状态是用ctl封装的,是一个AtomicInteger。

线程池状态做个简单复习:

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
// 低29位表示线程数量
private static final int CAPACITY   = (1 << COUNT_BITS) - 1; //即二进制11111111111111111111111111111 (29位)
 
// runState is stored in the high-order bits 线程池运行状态存储在32位的高3位
private static final int RUNNING    = -1 << COUNT_BITS; // 即二进制11100000000000000000000000000000(32位)
private static final int SHUTDOWN   =  0 << COUNT_BITS; // 即二进制00000000000000000000000000000000(32位)
private static final int STOP       =  1 << COUNT_BITS; // 即二进制00100000000000000000000000000000(32位)
private static final int TIDYING    =  2 << COUNT_BITS; // 即二进制01000000000000000000000000000000(32位)
private static final int TERMINATED =  3 << COUNT_BITS; // 即二进制01100000000000000000000000000000(32位)

这里指的是ctl是一个32位整数,并且将状态和数量放在了一起,用高3位表示状态,低29位表示数量。状态包括:

  • RUNNING:-1,高三位为111,线程池接受新任务且处理已入队任务
  • SHUTDOWN:0,高三位为000,不接受新任务,但处理入队任务STOP:1,高三位为001,不接受新任务,不处理入队任务,中断正在进行的任务
  • TIDYING:2,高三位为010,所有任务已完成,workerCount为0,转到TIDYING状态会执行terminated()钩子方法
  • TERMINATED:3,高三位为011,已完成状态

从execute方法中可以看出,当一个任务被执行时,线程池的工作流程是:

  1. 当核心线程池还不满,创建一个核心线程执行任务,addWorker的参数二为true
  2. 当核心线程池满,先尝试添加到阻塞队列,如果添加成功,再次检查线程池状态,如果线程池已经不能继续工作了,执行拒绝策略,如果可以工作且工作Worker数量为0,尝试创建非核心线程池线程执行任务。
  3. 如果阻塞队列都添加不成功,尝试创建非核心线程池线程执行任务,无法执行,则执行拒绝策略。

根据execute方法,我们可以获取到reject策略执行的时机:

  • 存入阻塞队列,触发于线程池状态变为非RUNNING状态并成功执行了任务移除流程
  • 添加非核心线程池失败

在本篇的开篇问题中,根据日志“queued tasks = 3”可知,代码的运行上下文并非线程池结束,而是因为长度为3的阻塞队列已经塞满,并且非核心线程也添加失败了。

由此我们提出以下几个问题,作为后续分析的方向:

  • RejectExecution的日志输出为什么会将alive threads输出为3?
  • 核心线程与非核心线程的添加流程是什么?为什么会添加失败?

本篇结论:

  • 线程池的提交方法 - execute方法执行的流程是:核心线程池 - 阻塞队列 - 非核心线程池
  • reject策略的触发点是:
    • 存入阻塞队列,触发于线程池状态变为非RUNNING状态并成功执行了任务移除流程
    • 添加非核心线程池失败
  • 6
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值