Java线程池ThreadPoolExecutor源码分析

101 阅读30分钟

线程池源码分析

分析目标

主要分析下面这个类:

ThreadPoolExecutor

image.png

hello world

public static void main(String[] args) throws Exception {
    ExecutorService pool = Executors.newFixedThreadPool(2);
    for (int i = 0; i < 10; i++) {
        pool.execute(() -> System.out.println(Thread.currentThread().getName() + ": hello world!!!"));
    }
    pool.shutdown();
}

接下来,我们将依据这段hello world代码进行大略的源码分析

源码分析

构造函数分析

/**
 * 
 * @throws IllegalArgumentException if one of the following holds:<br>
 *         {@code corePoolSize < 0}<br>
 *         {@code keepAliveTime < 0}<br>
 *         {@code maximumPoolSize <= 0}<br>
 *         {@code maximumPoolSize < corePoolSize}
 * @throws NullPointerException if {@code workQueue}
 *         or {@code threadFactory} or {@code handler} is null
 */
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

参数解析:

  1. corePoolSize(核心线程数):这是线程池想要一直保持的最小线程数,即使它们暂时没事做。除非你设置了allowCoreThreadTimeOut,否则这些核心线程会一直存在。
  2. maximumPoolSize(最大线程数):线程池能容纳的最大线程数量。当任务非常多,超过核心线程数时,线程池可以临时增加线程,但不会超过这个上限。
  3. keepAliveTime(空闲线程存活时间):如果线程池中的线程数量超过了核心线程数,那么这些额外的线程(非核心线程)在没有任务可执行时,会等待这么长时间,之后如果没有新任务来,它们就会被终止。换句话说,这是多余线程的等待期限。
  4. unit(存活时间单位):用来衡量keepAliveTime的单位,比如秒、毫秒等。
  5. workQueue(工作队列):一个存放待执行任务的队列。当你通过execute方法提交任务时,任务会先进入这个队列等待被执行。
  6. threadFactory(线程工厂):每当线程池需要创建新线程时,会用你提供的这个工厂来定制新线程,比如设置线程的名字、优先级等。
  7. handler(拒绝策略处理器):当线程池和队列都达到极限,再也无法接纳新任务时,会触发这个处理器来决定如何处理后续到来的任务,比如丢弃任务、抛出异常或让调用者自己处理等。

小白:“keepAliveTime怎么工作的?”

小黑:“这个参数具体功能可以在getTask中找到,等后续会具体分析”

小白:那上面的handler参数呢?

小黑:这是拒绝策略。线程池和队列都满了之后,则会执行handler拒绝策略。java提供了好几种拒绝策略:

  1. AbortPolicy(默认策略):想象一下,你去一家非常繁忙的餐厅吃饭,但餐厅已经满座,没有多余的位置给你了。这时,服务员直接告诉你“抱歉,没位置了”,这就是AbortPolicy。**当线程池太忙,新来的任务就像这位客人一样,会被拒绝,线程池通过抛出一个异常告诉提交任务的人:“我实在处理不了更多任务了”。**这时,提交任务的程序需要自己处理这个异常,比如记录下这个情况、尝试换一种方式处理任务,或者干脆放弃。
  2. CallerRunsPolicy:还是那个餐厅的例子,如果餐厅采用这种策略,相当于服务员说:“座位满了,但厨房给你,你可以自己煮。”也就是说,提交任务的线程自己充当厨师,直接在自己的线程里执行这个任务,不等餐厅(线程池)安排。这种方式适合那些任务执行起来比较快的情况,不会让提交任务的线程等太久,但要注意,这会占用提交任务线程的资源,可能会影响它原本要做的其他工作。
  3. DiscardPolicy:想象一下,你往邮筒里投信,但邮筒满了,邮局的策略是直接扔掉你的信件,不通知你也不尝试存留。DiscardPolicy就是这样,线程池满了之后,新来的任务直接被丢弃,不做任何处理,也不会告诉你。这种方法适用于那些即使丢掉任务也不会造成严重后果的场景。
  4. DiscardOldestPolicy:想象一下,图书馆的书架满了,你想借的新书放不下,图书馆决定撤下一本最旧无人问津的书来为你上架新书。DiscardOldestPolicy就是这样的策略,**线程池发现任务太多时,会先从任务队列中移除最老的一个任务(最早提交但还没执行的任务),然后尝试再次提交当前的新任务。**这背后的想法是,可能最新的任务更加紧急或者重要,所以牺牲旧任务来为新任务腾位置。

小黑:当然你也可以自定义实现RejectedExecutionHandler

execute源码分析

想象一下你经营一家快递分拣中心,execute方法就像是你接收并处理包裹的过程。

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    int c = ctl.get(); // 拿到 Worker线程数量(workerCount)和运行状态(runState)
    // 你看看现在有多少快递员正在工作(`workerCountOf(c)`),如果人数少于核心快递员(长期员工)数量(`corePoolSize`),意味着你可以聘请更多的核心快递员
    if (workerCountOf(c) < corePoolSize) {
        // 于是你尝试雇佣一个新快递员并把包裹直接交给他处理(`addWorker(command, true)`),如果成功,任务就算安排好了
        if (addWorker(command, true)) // 添加核心Worker
            return; // 添加完毕直接返回
        c = ctl.get(); // 再次拿到 Worker线程数量(workerCount)和运行状态(runState)
    }
    // 如果核心快递员都忙,但快递中心还在营业(isRunning(c)),可以先把包裹放到仓库(workQueue.offer(command)),等待有空闲的快递员来取。
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        // 放好包裹后,你再次确认快递中心是否还在营业。如果突然间决定歇业了(!isRunning(recheck)),你需要把刚放进去的包裹拿出来(remove(command)),因为它现在送不出去了。这时候,你会按照预先设定的规则处理这个包裹,比如通知寄件人快递延误(reject(command))
        if (!isRunning(recheck) && remove(command))
            // 执行拒绝策略处理器;这里就是上面构造函数的handler参数
            reject(command);
        // 如果快递中心还在营业,但发现竟然没有快递员在工作了(workerCountOf(recheck) == 0),这意味着得紧急增派一个非核心快递员(临时工)来处理积压的包裹(addWorker(null, false))
        else if (workerCountOf(recheck) == 0)
            // 添加一个非核心线程
            addWorker(null, false);
    }
    // 试试看能不能往线程池里添加非核心线程和任务
    else if (!addWorker(command, false))
        // 添加失败
        reject(command);
}

小白:上面的添加一个非核心线程 addWorker(null, false); 为什么不把command 传递进去?

小黑:isRunning(c) && workQueue.offer(command)已经在快递分拣中心仓库中存着了

总结:

这个函数的本质在于如何去处理command这个任务。

我们从这个函数中可以得出command有三种处理方法,第一种就是直接添加到work工作线程中运行,第二种方法就是添加到work队列中等待后续其他线程的读取并执行,第三种方法就是各种条件都不满足,直接拒绝执行command

知道这些之后,我们再对execute函数中的其他函数逐一分析

详解ctl字段

小白:字段ctl的作用是什么?能说说么?

小黑:这个字段的作用主要有两个

  1. 工作者线程数量(workerCount:告诉我们在外面忙碌的工人(线程)有多少个。为了节省空间,它限制了最大工人数为5亿左右,虽然理论上能表示的更大,但这样能让处理更快更简单。
  2. 运行状态(runState:表明线程池现在是在**RUNNINGSHUTDOWNSTOPTIDYING还是TERMINATED**。这个状态决定了线程池接受新任务与否,是否处理队列里的任务,以及是否中断正在进行的任务。

小黑:ctl的状态变化控制了线程池的一生:

  • RUNNING:欢迎新任务,继续处理队列里的活。(-536870912)
  • SHUTDOWN:不接新活了,但会把答应好的做完。(0)
  • STOP:新活旧活都不接了,还试图中断那些已经开始的活。(536870912)
  • TIDYING:所有活都干完了,工人都走了,准备打扫战场(调用terminated()清理方法),说明线程池已经准备关闭了,也就不再接收任何任务和创建新的Worker。(1073741824)
  • TERMINATED:打扫干净了,彻底收工。(1610612736)

小白:那要怎么读取呢?

小黑:有两个函数workerCountOfrunStateOf

private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int COUNT_MASK = (1 << COUNT_BITS) - 1;
// Packing and unpacking ctl
private static int runStateOf(int c)     { return c & ~COUNT_MASK; }
private static int workerCountOf(int c)  { return c & COUNT_MASK; }

这里定义了COUNT_BITS,它的值是Integer.SIZE - 3。在Java中,Integer.SIZE表示一个int类型的变量占用的位数,通常是32位(因为Java的int是32位的)。因此,COUNT_BITS的计算结果是32 - 3 = 29。这意味着有29位被分配用来专门计数,可能是线程数量或者其他某种计数目的。

基于新的COUNT_BITS值(29),COUNT_MASK的生成方式也相应变化。(1 << COUNT_BITS)表示将1左移29位,得到一个二进制数,其形式为1后面跟着29个0(即1000...000,共32位,其中29个0)。减去1之后,最右边的29位都变成了1,形成一个二进制掩码,如0111...111(共29个1)。

COUNT_MASK = 00011111111111111111111111111111

前面三个000位置存储的是状态,后面的几个1都是存放的Worker

详解addWorker对象

想象你的快递分拣中心有一个灵活的雇用策略,用来管理快递员(工作线程)的数量,确保包裹(任务)能够高效处理。

int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
    if (addWorker(command, true))
        return;
    c = ctl.get();
}

上面这段代码往workers集合中添加新的核心Worker并且将任务(Command)安排上,如果添加成功,则直接返回

那么需要分析addWorker,但你会发现,这个函数也过于复杂了。

首先我们关注这个函数的注释:

  • 雇用原则:当决定是否雇用新快递员时,addWorker就像是人力资源经理在评估当前的业务需求和公司政策。它首先看分拣中心是否还在扩张阶段(对应线程池还在积极接受任务),或者是否达到了非常繁忙以至于需要加班的程度(任务队列满了,需要额外人手立即处理任务)。
  • 遵守规则:这个经理会检查公司的雇用规则。如果公司规定了核心快递员的数量(核心线程数),并且继续扩大团队,或者双11时,公司在保证不亏损还能盈利的状态下临时聘请快递员到最大人数(最大线程数)。core这个参数就像是经理手中的指南针,告诉他现在是聘请长期员工(核心员工)还是聘请临时工。
  • 实际操作:如果一切条件符合,经理就会发布招聘广告(使用线程工厂创建新线程),并且新来的快递员一到岗就能立马接手第一个包裹(firstTask),这能有效减少等待时间,提高效率。如果招聘不成功,比如资金紧张(内存不足)导致无法支付工资(创建线程失败),那么经理会取消这次招聘,确保不会因为尝试雇人而混乱了原有的人员配置。
  • 特殊情况处理:如果分拣中心已经决定要关门大吉(线程池停止或即将关闭),或者招聘部门(线程工厂)出了问题,无法提供合适的快递员,那么这次招聘自然就失败了,addWorker会返回false,表示新快递员没能加入。
  • 成功反馈:如果新快递员顺利加入并且开始工作,addWorker会高兴地告诉大家(返回true),表示队伍又壮大了,可以处理更多的包裹。
private boolean addWorker(Runnable firstTask, boolean core) {
    // 我们将源码分为几个部分分析
}

第一段代码:

retry:
for (int c = ctl.get();;) {
    // 你看看分拣中心是否已经关闭或正在关闭中(`runStateAtLeast(c, SHUTDOWN)`),并且确认即使有新任务(`firstTask != null`)或者任务队列已经空了(意味着没有待处理的包裹),也不应该再招人了,因为关闭状态下不接受新任务(firstTask)。
    if (runStateAtLeast(c, SHUTDOWN) && (runStateAtLeast(c, STOP) || firstTask != null || workQueue.isEmpty()))
        return false;
    for (;;) {
        // corePoolSize & COUNT_MASK, 三个0,29个1,做 & 操作便是取 worker 数量,忽略线程池状态的三个0
        // 你检查是否已经达到了核心快递员数量上限(如果招聘的是核心快递员)或者总快递员数上限(如果招聘的是非核心的临时快递员)。
        if (workerCountOf(c)
            >= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK))
            return false;
        // 如果没有超过,尝试增加快递员数,如果成功(通过原子操作`compareAndIncrementWorkerCount(c)`),就可以跳出循环继续招聘流程。
        if (compareAndIncrementWorkerCount(c))
            break retry;
        c = ctl.get();  // Re-read ctl
        if (runStateAtLeast(c, SHUTDOWN))
            continue retry;
        // else CAS failed due to workerCount change; retry inner loop
    }
}

上面这段代码的核心在于cas添加Worker的数量

分析到这里,我们已经发现了两次 return false,一次是线程池关闭,所以返回false,一次是worker超出上限,所以返回false

同时第二个参数boolean core的任务完成了,后续没有任何关于它的代码。

添加完毕之后,我们分析下面这段正式聘请员工的代码:

boolean workerStarted = false; // 标记Worker是否已经调用过start函数,启动线程
boolean workerAdded = false; // 标记worker是否添加到workers线程池中
Worker w = null; // worker线程
try {
    // 你准备招聘一名新快递员,并为他分配第一个包裹(创建`Worker`实例)。
    w = new Worker(firstTask); 
    final Thread t = w.thread; // 拿到线程
    if (t != null) {
        // 获取可重入锁
        final ReentrantLock mainLock = this.mainLock;
        // 上锁,这里的锁主要保护的对象:workers
        mainLock.lock();
        try {
            // Recheck while holding lock.
            // Back out on ThreadFactory failure or if
            // shut down before lock acquired.
            int c = ctl.get();
			// 线程池正常运行
            // 线程池是正常状态 或者 SHUTDOWN 状态,但是这两个状态都会处理workQueue中剩下的对象
            // 然后继续判断没有新的Task进入workQueue中,这两种情况下,才会执行后续代码
            // 做个比喻:如果厨房(线程池)还在忙碌地营业(RUNNING),或者即便我们已经在准备打烊(SHUTDOWN)但还没正式关门,并且此刻没有新的菜肴(firstTask)等待上炉,那么厨师(Worker线程)就可以继续他们的工作任务。(w = new Worker(firstTask); 已经将firstTask添加到w中,所以在SHUTDOWN状态还可以继续执行后续代码)
            // 再次检查分拣中心是否仍在正常运营或是在有序关闭中且没有新包裹。
            if (isRunning(c) ||
                (runStateLessThan(c, STOP) && firstTask == null)) {
                // 如果线程状态不是新建状态,表明线程已经启动,跑起来了,那么直接抛出异常
                if (t.getState() != Thread.State.NEW)
                    throw new IllegalThreadStateException();
                // 如果一切正常,正式登记他为分拣中心的员工(将`Worker`加入`workers`列表)
                workers.add(w);
                // 并记录这次招聘使得员工总数达到了历史最高
                workerAdded = true;
                // 拿到员工的数量
                int s = workers.size();
                // 检查当前线程池中的线程数量是否超过了之前记录的最大规模(largestPoolSize)
                if (s > largestPoolSize)
                    // 跟踪线程池在其生命周期中所达到的最大并发线程数
                    largestPoolSize = s;
            }
        } finally {
            // 解锁
            mainLock.unlock();
        }
        // worker已经添加到线程池中
        if (workerAdded) {
            // 启动 worker线程执行 firstTask 的任务
            t.start();
            // 证明 worker 已经启动
            workerStarted = true;
        }
    }
} finally {
    // 如果在任何环节发现不能成功招聘(比如新快递员突然反悔,即workerStarted为false),你会清理已做的招聘准备,比如取消新快递员的名额,确保一切回归原状。
    if (! workerStarted)
        // 释放前面cas增加的线程数量,也将worker从workers中删除
        addWorkerFailed(w);
}
return workerStarted;

总结:这段代码的核心就是,在确保线程池状态允许、不超过线程池大小限制的情况下,安全地添加一个新的工作线程,并尝试启动它来执行任务,同时处理了招聘失败的异常情况,确保线程池状态的一致性和正确性。

分析addWorkerFailed

在尝试招聘一名新快递员(创建Worker对象并准备启动工作线程)时,不幸遇到了问题,比如资金不足(内存问题)导致无法正式录用这名快递员。这时,你需要进行一系列的善后工作,比如拿回员工工牌等,确保分拣中心的管理记录和资源配置依然准确无误。这便是addWorkerFailed方法的作用。

private void addWorkerFailed(Worker w) {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        if (w != null)
            // 如果那名未能成功入职的快递员信息还在你的招聘系统名单上(w != null),你需要立即将他从名单上划掉(从workers列表中移除这个Worker实例),就好像他从未被考虑过一样
            workers.remove(w);
        // 既然这名快递员并未真正加入,你需要在总人数上减去这个虚增的数字,确保统计的快递员数量(线程池的线程数量)是准确的。
        decrementWorkerCount();
        // 鉴于招聘失败可能意味着分拣中心遇到了资金问题,你可能需要重新评估是否应该提前结束一天的工作(调用tryTerminate来检查是否可以安全地关闭线程池)
        tryTerminate();
    } finally {
        mainLock.unlock();
    }
}

分析tryTerminate

继续沿用快递分拣中心的比喻,这段话描述的是如何在合适的时机正式结束分拣中心的运营,并完成所有收尾工作,使其进入“彻底关闭”状态。

想象一下,你作为分拣中心的负责人,现在要确保在满足特定条件时,能够安全、有序地关闭整个中心。

有两种情况下,你会考虑将分拣中心彻底关闭(进入TERMINATED状态):

  • 分拣中心已经宣布即将关门(SHUTDOWN状态),并且所有快递员(Worker)都已经下班回家了,同时,等待分拣的包裹也全部处理完毕,架子上空无一物(任务队列为空)。
  • 或者是紧急关闭指令下达(STOP状态),即便还有包裹未来得及分拣(任务队列非空),你也必须立即让所有快递员停止工作,准备关门。

如果发现中心应该关闭了,但是还有快递员在工作(活动线程存在),你不会马上锁门。而是先选择一个正在休息的快递员(中断一个空闲线程),告诉他立刻停止工作,并把关门的消息传给其他还在忙碌的快递员。这样做是为了确保每个快递员都知道即将关门,加快收尾速度。

这个关闭操作(tryTerminate)不是随意进行的,只有在减少了快递员数量或者清空了等待分拣的包裹(worker queue为空)后,你才会考虑执行这一步骤。这意味着,在实际操作中,你需要在做了一系列收尾动作,确认可以安全关闭后,才调用这个流程。

这个关门的操作(tryTerminate)不仅仅限于你自己使用,像是负责定时任务的分拣小组(ScheduledThreadPoolExecutor)也可以用这个流程来管理他们那边的关闭过程,确保整个快递公司的各个部门都能够有序、统一地处理关闭事宜。

final void tryTerminate() {
    for (;;) {
        int c = ctl.get();
        // 如果分拣中心还在忙碌中(isRunning(c)),显然现在不适合关门,直接回家。
        // 如果已经处于打扫卫生阶段(TIDYING状态),意味着关闭流程已经启动,无需重复操作。
        // 如果还没到强制关闭时间(< STOP状态,中心可能处于RUNNING或SHUTDOWN),并且还有包裹等着分拣(!workQueue.isEmpty()),那也得继续工作,不能关。
        if (isRunning(c) ||
            runStateAtLeast(c, TIDYING) ||
            (runStateLessThan(c, STOP) && ! workQueue.isEmpty()))
            return;
        // 如果上述条件都不满足,说明可能到了可以考虑收工的时候,但你发现还有快递员在(workerCountOf(c) != 0)。
        // 你决定先礼貌地打断一个正在休息的快递员(interruptIdleWorkers(ONLY_ONE)),让他帮忙加速完成剩余工作,然后你再考虑是否关门
        if (workerCountOf(c) != 0) { // Eligible to terminate
            // 中断一条空闲的工作线程
            interruptIdleWorkers(ONLY_ONE); // ①
            return;
        }

        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            // 如果确定没有人正在处理包裹,也没有新包裹进来(相当于线程池状态可以安全过渡到TIDYING),你就尝试挂上“正在打扫”的牌子(ctl.compareAndSet(c, ctlOf(TIDYING, 0)))
            if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
                try {
                    // 成功挂上牌子后,你开始指挥最后的清理工作(调用terminated()),比如整理工具、关闭设备。
                    terminated();
                } finally {
                    // 清理完毕后,挂上“已关闭”的永久牌子(状态变为TERMINATED),并敲锣打鼓(termination.signalAll())通知所有还在等待的同事,我们可以正式下班了。
                    ctl.set(ctlOf(TERMINATED, 0));
                    // 唤醒所有被mainLock锁定的线程
                    termination.signalAll();
                }
                return;
            }
            // 如果在尝试挂牌子时发现有人抢先了一步(CAS操作失败),你不会强行,而是回到第一步重新评估情况,确保在正确的时间以正确的方式关闭分拣中心。
        } finally {
            mainLock.unlock();
        }
        // else retry on failed CAS
    }
}

:在复杂的线程池管理中,尤其是当线程池处于关闭或停止状态时,可能有一些线程仍在忙碌执行任务,特别是如果这些线程长时间运行或被阻塞,它们可能不会立即意识到线程池的状态变化。通过中断一个空闲线程,可以作为一个触发点,这个线程在被中断后可能会执行一些清理工作,或者在代码中通过响应中断来进一步传播终止的意图,比如调用某些回调函数、设置内部标志等,进而间接地通知到其他线程或执行额外的关闭逻辑。

分析interruptIdleWorkers

想象你的快递分拣中心快要下班了,大部分快递员都已经完成工作开始休息,但还有少数人在等待最后一批包裹的到来。这时,你想通知大家今天工作结束,可以回家了。interruptIdleWorkers就像是你作为经理,去通知那些正在休息的快递员们的方式

你巡视一圈,发现有些快递员正在休息区等待新的任务。interruptIdleWorkers就是这样一个过程,它专门找到这些没有在忙(空闲线程)的快递员,告诉他们(尝试中断)可以结束工作回家了。

如果设置了onlyOne=true,你决定一次只叫醒一个快递员。这样做的目的是,即使大多数人已经知道下班的消息,至少还有一个快递员能收到通知,并且他可以和其他还在等待的同事说:“嘿,收工了!”确保消息能传遍整个休息区(这条线程会设置线程中断状态)

有时候,可能你去叫某位快递员时,他因为某些原因(比如他在安全区域,不允许别人随便打扰,类似于SecurityException)没有立即响应你。这时,你不会在这里停留太久争执,而是继续去通知下一个快递员。这样虽然不是每个人都能第一时间被叫醒,但至少能确保有一部分快递员能够收到下班的通知,然后他们可以互相转告,最终所有人都能知道下班的信息。

private void interruptIdleWorkers(boolean onlyOne) {
    final ReentrantLock mainLock = this.mainLock;
    // 确保在操作线程集合时的线程安全
    mainLock.lock();
    try {
        // 你开始查看所有快递员的名单(遍历workers),对每位快递员进行检查
        for (Worker w : workers) {
            Thread t = w.thread;
            // 你先看快递员(线程)是否已经被通知过下班(是否被中断),以及是否能直接和快递员沟通(尝试获取快递员对应的Worker锁)。
            if (!t.isInterrupted() && w.tryLock()) {
                try {
                    // 如果快递员既没被通知过下班,你又能和他沟通上,你就会轻轻地拍他一下(调用t.interrupt()),告诉他可以准备收工了。即使在拍他的过程中遇到些小麻烦(抛出SecurityException),你也选择忽略这个阻碍,继续去通知下一位。
                    t.interrupt();
                } catch (SecurityException ignore) {
                    // 即便中断操作抛出了`SecurityException`,也选择忽略它,继续处理下一个线程
                } finally {
                    w.unlock();
                }
            }
            // 如果`onlyOne`为`true`,遍历会在中断了第一个线程后立即停止。
            // 如果老板只让你叫醒一个快递员(onlyOne为true),那么在成功通知到第一个快递员后,你就不再继续通知其他人,直接结束这次通知任务。
            if (onlyOne)
                break;
        }
    } finally {
        mainLock.unlock();
    }
}

分析terminated

这个函数提供了一个钩子(hook)或扩展点,允许子类在线程池终止后执行一些自定义的清理或后续操作。

/**
 * Method invoked when the Executor has terminated.  Default
 * implementation does nothing. Note: To properly nest multiple
 * overridings, subclasses should generally invoke
 * {@code super.terminated} within this method.
 */
protected void terminated() { }

当线程池完成了它的工作,所有任务执行完毕,所有工作线程已经停止,并且线程池状态转移到了TERMINATED状态时,terminated()方法会被调用。这为开发人员提供了一个机会,去执行一些资源清理、日志记录或是其他任何在关闭线程池后需要完成的工作。

如果子类想要override这个方法并添加自己的实现,通常应该先调用超类(superclass)的terminated()方法,以确保基类中可能有的任何默认或必要行为得以执行。这是一种良好的面向对象编程实践,确保了类的继承和扩展性。

分析t.start()函数启动的Worker

w = new Worker(firstTask);
final Thread t = w.thread;

在上面这段代码中,Worker的构造函数被调用

Worker(Runnable firstTask) {
    // 将状态设置为-1的目的是作为一个标记,用来“禁止中断直到runWorker”。这意味着在Worker线程开始执行其主要工作循环(即runWorker方法)之前,不希望线程因为中断而停止。
    // 这是一种保护措施,确保线程能够安全地初始化并准备好执行任务,而不会受到外界中断的干扰。
    setState(-1); // inhibit interrupts until runWorker
    this.firstTask = firstTask;
    // 这段代码的效果在addWorker函数的t.start()得到体现,这样之后才会去调用worker的run函数。
    this.thread = getThreadFactory().newThread(this);
}

分析run函数

public void run() {
    // 祝你这里将this的传递到runworker函数中。
    runWorker(this);
}

分析runWorker函数

想象你管理的快递分拣中心有一群勤劳的快递员(线程),他们的主要工作就是从等待分拣的任务队列中领取包裹(任务)并完成分拣。

  • 开始工作:每当一个快递员(线程)开始他的工作日,如果分配给他一个初始的包裹(即线程创建时带有的任务),他会直接开始处理这个包裹。这就好比快递员一上班就有个指定的包裹等着他去分拣。
  • 持续分拣:一旦完成手头的任务,快递员不会停下来休息,他们会自动回到任务队列前,通过一个叫做getTask的过程寻找下一个包裹。这个过程就好比快递员不断地检查队列,看有没有新的包裹需要分拣。
  • 任务耗尽与退出:但是,如果任务队列里没有包裹了(getTask返回null),快递员不会傻站着等,他们会意识到可能是快递中心调整了工作计划(线程池状态变化或配置调整),比如因为订单减少决定提前下班。这时,快递员会结束自己的工作,相当于线程优雅地退出,不再占用资源。

runWorker方法就是指导一个快递员如何从上班、工作到下班的全过程

final void runWorker(Worker w) {
    // 获得快递员
    Thread wt = Thread.currentThread();
    // 查看快递员今天上班时分配的第一个包裹任务
    Runnable task = w.firstTask;
    // 之后,这个初始任务就被标记为已完成,以便快递员接下来可以领取新的任务
    w.firstTask = null;
    // 告诉快递员先别忙活,先查看中心公告,查看是否有领导安排其他任务(比如中断)
    w.unlock(); // allow interrupts
    // 这个标记用来记录快递员是否正常完成了今天的工作,如果中间出了什么岔子(比如身体不适提前下班),这个标记就会变成false
    boolean completedAbruptly = true;
    try {
        // 快递员开始进入工作循环,只要还有包裹(任务)需要分拣,他就一直工作下去。
        // 包裹来源有两个:一是上班时分配的初始任务,二是通过getTask方法从任务队列中获取的新包裹。
        while (task != null || (task = getTask()) != null) {
            w.lock();
            // 首先判断线程池是否正在停止(runStateAtLeast(ctl.get(), STOP)),如果是,说明需要中断线程。
            // 由于中断可能在多个操作间发生,这里使用(Thread.interrupted() && runStateAtLeast(ctl.get(), STOP))再次确认线程是否已被中断并且线程池处于停止状态。这是因为如果线程之前被中断过,在执行到这一步前可能已经被清除中断状态,所以需要重新检查确保在正确的条件下中断线程。
            if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                    // 对于每一个包裹(任务),快递员先做一些准备工作(beforeExecute),
                    beforeExecute(wt, task);
                    // 然后开始派送(task.run())
                    task.run();
                    // 派送完后再做一些收尾工作(afterExecute)。
                    afterExecute(task, null);
                } catch (Throwable ex) {
                    // 如果派送过程中遇到问题(比如包裹破损),会记录问题并在收尾工作时报告,让快递员直接停止工作(抛出异常)。
                    afterExecute(task, ex);
                    throw ex;
                }
            } finally {
            	// 每完成一个包裹,快递员会更新自己完成的任务数,并释放当前任务的锁定,以便能继续领取下一个任务
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
    	// 如果所有任务都顺利完成了,completedAbruptly会被设为false,表示快递员是正常完成工作的
        completedAbruptly = false;
    } finally {
        // 管理员会根据completedAbruptly的值来决定是否需要对快递员进行特别处理,比如因为异常提前下班的快递员可能需要有人替补。
        processWorkerExit(w, completedAbruptly);
    }
}

分析processWorkerExit

假设这是一个临时聘请厨师的餐厅,厨师的工资都是日结,第二天重新聘请厨师。

private void processWorkerExit(Worker w, boolean completedAbruptly) {
    // 如果快递员是因为在送快递途中遇到意外(比如包裹损坏,对应completedAbruptly=true),经理需要手动调整还在工作的快递员人数记录,相当于告诉系统:“有个人突然走了,得减掉一个。”
    if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
        decrementWorkerCount();
	
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        // 统计这位快递员完成的派送量(completedTaskCount)
        completedTaskCount += w.completedTasks;
        // 在快递员名册上划掉他的名字(workers.remove(w))
        workers.remove(w);
    } finally {
        mainLock.unlock();
    }
	// 经理会评估是否可以关门歇业了(tryTerminate())。如果所有的包裹都送完了,没有新的订单,也没有顾客在等待,那就可以考虑结束一天的工作或者准备迎接第二天的运营。
    tryTerminate();

    // 如果不是因为关门歇业(即配送中心还在运营),经理会考虑是否需要招新人来顶替离职的快递员。
    int c = ctl.get();
    if (runStateLessThan(c, STOP)) {
        // 检查快递员是否是正常离职(没有因为突发问题,比如包裹损坏)。如果快递员是正常完成任务后离开的,我们才进入下一步考虑是否招新快递员
        if (!completedAbruptly) {
            // 这里决定最低需要多少快递员留在岗位上。如果公司政策允许核心快递员(那些即使没活儿也要待命的快递员)有空闲时间(allowCoreThreadTimeOut为真),那么理论上可以降到0。否则,最少需要保留之前设定的核心快递员数量(corePoolSize)
            int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
            // 如果政策允许核心快递员休息,并且当前正好没有快递员待命(min设为了0),但还有包裹等待派送(任务队列非空),这时至少需要一个快递员来处理这些包裹,所以将min调整为1。
            if (min == 0 && ! workQueue.isEmpty())
                min = 1;
            // 检查当前在职的快递员数量是否已经满足了最低要求(不管是核心快递员还是临时增补的)。如果足够了,就不需要额外招人,直接结束这次处理流程。
            if (workerCountOf(c) >= min)
                return; // replacement not needed
        }
        // 如果前面的检查结果显示确实需要新快递员,就通过addWorker方法来雇佣一个新快递员。这里的null意味着新快递员加入时不带特定任务(他将等待任务队列中的新包裹分配),而false表示不希望因为这次添加新快递员而导致线程池扩容超过最大限制(如果有这样的设定)
        addWorker(null, false);
    }
}

疑问和解答

keepAliveTime是怎么决定空闲非核心线程的寿命的?

关键代码在源码的getTask函数中的timedtimedOut局部变量

timed == true 决定是否是非核心线程

timedOut == true 表明超时了(keepAliveTime

getTask中有这么一段代码:

private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?

    for (;;) {
        int c = ctl.get();

        // Check if queue empty only if necessary.
        if (runStateAtLeast(c, SHUTDOWN)
            && (runStateAtLeast(c, STOP) || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }

        int wc = workerCountOf(c);

        // Are workers subject to culling?
        // 忽视 allowCoreThreadTimeOut
        // wc > corePoolSize:当然线程大于核心线程,意味着 timed 为 true 的话 说明存在非核心线程
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
		// 忽视wc > maximumPoolSize 看 (timed && timedOut) 既是非核心线程也超时了
        if ((wc > maximumPoolSize || (timed && timedOut))
            // 并且 当前线程大于 1 和 任务队列为空的情况下才会去减少 worker 的数量
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }

        try {
            // timed 为 true 表明为 非核心线程
            // 从任务队列中获取任务,等待 keepAliveTime 时间
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            // 如果获取成功下面这段代码根本不会执行
            // 获取失败表明超时
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

小白:这样也没把workerworkers集合中删除啊?

小黑:前面分析了老半天源码,你白看了。还记得processWorkerExit函数么?

小白:记得啊

小黑:那你还不明白。processWorkerExit函数一开头将执行了workers.remove(w);的函数,至于后续的Worker替换是有先决条件的。if (workerCountOf(c) >= min)它会判断最低数量,如果满足则添加替换Worker否则直接被删除了