拆轮子:全面剖析 ThreadPoolExecutor(3)-庖丁解牛

本文目录:

概述

做了两章的热身,这里正式进入重点,开始对整个 ThreadPoolExecutor 的源码进行剖析,去理解它的原理与机制。

控制量

我们平时使用 Java 编程时,如果有状态操作,一般都会创建一个 int 值来表示。然后其实状态也就几种。然后还有其他的变量,比如记录数量等等,又会开一个新的 int 值等等。这样做的好处是可读性很好。JDK 源码中则不然,经常会出现一些通过位操作来记录和计算一些状态变量,效率高而且省内存。

这里的 ThreadPoolExecutor 就是这样做的。

记录方式

一个线程池,有了生命周期,有了执行任务的 Worker,就必然需要一些状态变量进行记录。又因为该线程池对象是可以被多个线程访问的,所以又要保证线程安全。

作者 Doug Lea 给了一个非常节省布料,又能满足线程安全的实现方式。再补充一句,除了省内存又安全,效率还高。那就是通过位运算来记录状态和某些变量。这个在 C&C++ 中是很常见的。同时这些在 JDK 或者 Android 源码中经常可以见到。这种处理状态位的方式,可读性差但性能高效。而且可读性差也是相对的,等我们吃透这个设计套路,读起来一点都不费劲。

在每个 ThreadPoolExecutor 实例都会持有一个成员 ctl:

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

这个 ctl 是个 AtomicInteger,意味着对该变量的操作,使用了 CAS 的方式确保了线程安全。初始化为 RUNNING 状态,Worker 数量为 0。

这个AtomicInteger 的 32 位被分割成两个部分

  • workerCount,低29位。就是有效的工作线程的数量。
  • runState,高3位。表示当前线程池的状态,具体的状态在声明周期中有说明,比如是否是 RUNNING,或者处于 SHUTDOWN 等等。

可以这样表示:

  0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|runState|       workerCount                                                                       |
+--------+-----------------------------------------------------------------------------------------+

runState 和 workerCount 这两个变量在同一个 Integer 中,源码中是通过位移来实现的。具体是怎么操作的?

我们来看定义的两个常量:

private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

有个 COUNT_BITS 表示 wokerCount 的位数,计算一下就是 29;CAPACITY 表示 workerCount 的最大值,也就是 2^29 - 1。从二进制上看,就是 29 个 1,后面可以用来做掩码,算出 runState 和 workerCount 各自的值。

runState 有 3 位可以表示 2^3 = 8 种状态。这里使用它来表示线程池生命周期中的 5 种状态:

状态 位移计算(Int) 高三位二进制表示
RUNNING -1 << COUNT_BITS 111
SHUTDOWN 0 << COUNT_BITS 000
STOP 1 << COUNT_BITS 001
TIDYING 2 << COUNT_BITS 010
TERMINATED 3 << COUNT_BITS 100

计算方式

ThreadPoolExecutor 提供了以下工具方法,用来操作 ctl:

ctl 的打包与拆包

打包表示把 runState 和 workerCount 合并成 ctl 的值。拆包表示分别读出这两个值。

获取 runState 的值,使用 CAPACITY 的反做掩码对 c 进行与操作获得:

private static int runStateOf(int c) {
    return c & ~CAPACITY;
}

获取 workerCount 的值,使用 CAPACITY 做掩码对 c 进行与操作获得:

private static int workerCountOf(int c) {
    return c & CAPACITY;
}

合并 runState 和 workerCount 为 c ,采用或操作:

private static int ctlOf(int rs, int wc) {
    return rs | wc;
}

所以我们对 ctl 的初始化 ctlOf(RUNNING, 0) ,就是初始化为 RUNNING,并且线程数为 0。

runState 的状态判断

因为运行状态在高 3 位,所以后面低 29 位不会影响判断。

private static boolean runStateLessThan(int c, int s) {
    return c < s;
}

private static boolean runStateAtLeast(int c, int s) {
    return c >= s;
}

private static boolean isRunning(int c) {
    return c < SHUTDOWN;
}

workerCount 的增减

直接对 ctl 进行加和减操作,这个时候 AtomicInteger 的特点就表现出来了。由于可能会有多个线程操作这个共享变量,AtomicInteger 可以应用自己 CAS 的特点保证线程安全。

private boolean compareAndIncrementWorkerCount(int expect) {
    return ctl.compareAndSet(expect, expect + 1);
}

private boolean compareAndDecrementWorkerCount(int expect) {
    return ctl.compareAndSet(expect, expect - 1);
}

线程安全的保证

这里的线程安全,不是指线程池内的线程的线程安全,这个是业务方控制的,线程池不负责。

这里的线程安全,是线程池被其他不同线程调用时的线程安全处理,线程池的一些成员变量,是会被其他线程访问到的,可能是线程池自己的线程也可能是外部的线程。需要考虑线程安全的有这几个 :

  • ctl
  • workers
  • largestPoolSize
  • completedTaskCount

其中 ctl 是 AtomicInteger,使用 CAS 的方式来实现线程安全。其余被一个 ReentrantLock 保护(其实这个底层也是 CAS 实现线程安全)。

AtomicInteger

控制量 ctl 里面有两个数据,运行状态 runState,有效的工作线程数量 workerCount。这个状态量是会被多线程操作的,需要进行同步。

同步的方式有很多种,可以直接使用 synchronized 简单粗暴的互斥,也可以用 ReentrantLock。

这里作者使用了 AtomicInteger,也就是 CAS 的方式来实现线程安全。然后使用乐观锁中的自旋锁来做线程安全处理,于是在代码中,操作 ctl 的地方都可以看到自旋的影子。

什么是自旋?

其实就是死循环,每次进行 CAS 操作,如果有其他线程竞争的话会返回失败,这时候进入下一个循环再来一次。如果对该变量竞争非常惨烈,导致长时间循环,这也是消耗 CPU 资源的。但是一般不会有这样的场景,有的话,可以反思一下业务设计是否合理。

使用 CAS 加自旋和直接使用 Synchronized 的效率从 JDK 1.6 已经比较接近了。一个从代码层面进行控制,一个在字节码层面使用 monitor 指令加锁。不过 CAS 的方式效率还是比 Synchronized 高。而且我们可以继承 AQS(AbstractQueueSynchronizer) 来实现很多自定义的锁,像 ThreadPoolExecutor 的内部类 Worker 也使用 AQS 来实现不可重入锁。所以 CAS 的方式更加灵活。

ThreadPoolExecutor 有对 ctl 进行操作的地方,基本都会有自旋的操作。比如创建并执行线程的方法 addWorker :

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

    ...
    for (;;) {
        ...
        if (compareAndIncrementWorkerCount(c))
            break retry;
        c = ctl.get();  // Re-read ctl
        if (runStateOf(c) != rs)
            continue retry;
        // else CAS failed due to workerCount change; retry inner loop
    }
}

比如减少工作线程数量的方法 decrementWorkerCount:

private void decrementWorkerCount() {
    do {} while (! compareAndDecrementWorkerCount(ctl.get()));
}

这种不使用独占锁 Synchronized 的方式,有效率上的提升。

ReetrantLock

线程的状态 runtState 和线程数量 workerCount 在控制量 ctl 中,已经由 AtomicInteger 提供的 CAS 进行保护了。

其他的变量呢?比如线程工作队列 workQueue ?workQueue 是非线程安全集合 HashSet,所以应该有其他的加锁机制来进行线程安全处理。

我们从源码中找到了 mainLock 变量,ReetrantLock 的实例。就是用来对线程工作队列和相关的变量进行加锁:

private final ReentrantLock mainLock = new ReentrantLock();

它保护了这几个变量:

  • BlockingQueue<Runnable> workerQueue ,任务队列。
  • largestPoolSize ,线程池达到过的最大的工作线程数量。
  • completedTaskCount ,已经结束的任务数。

比如获取活动的线程数 ,需要遍历工作线程队列,所以有这样的代码:

public int getActiveCount() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        int n = 0;
        for (Worker w : workers)
            if (w.isLocked())
                ++n;
        return n;
    } finally {
        mainLock.unlock();
    }
}

使用 mainLock 对工作线程队列 workes 的访问进行互斥,避免因为多线程并发造成问题。

同时,还加了一个 Condition 用来支持等待关闭功能:

private final Condition termination = mainLock.newCondition();

这个条件用在 awaitTermination 方法中。因为当外界执行 shutdown 或者 shutdownNow 方法关闭线程池的时候,线程池还会做些收尾才最终结束。想要真正拿到最后关闭的通知,需要调用 awaitTermination 来等待。

运行机制

execute

线程池执行任务的入口。要任务都被封装在 Runnable 中。线程池根据自己的状态来判断要怎么去执行这个 Runnable。

完整注释

线程池 execute 方法的源码代码加上我理解后的注释如下:

    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        // 获取控制量的值,控制量 ctl = runState | workerCount。
        int c = ctl.get();
        // 判断工作线程数是否小于核心线程数。
        if (workerCountOf(c) < corePoolSize) {
            // 创建核心工作线程,当前任务作为线程第一个任务,使用 corePoolSize 作为最大线程限制。
            if (addWorker(command, true))
                return;
            // 创建核心工作线程失败,重新获取控制量的值
            c = ctl.get();
        }
        // 判断线程池是否是运行中的状态,是的话将线程加入任务队列
        if (isRunning(c) && workQueue.offer(command)) {
            // 再次获取控制量的值,用来做 double-check
            int recheck = ctl.get();
            // 判断线程是否已经不是运行状态了,不是运行状态尝试从任务队列中把任务移出
            if (!isRunning(recheck) && remove(command))
                // 移出任务成功后,使用执行拒绝策略
                reject(command);
            /*
             * 如果满足下面条件这一:
             * 1. 线程是运行状态
             * 2. 任务无法从任务队列移出,
             * 判断工作线程数量
             */
            else if (workerCountOf(recheck) == 0)
                /*
                 * 工作线程数量为 0,
                 * 创建工作线程,用 maximumPoolSize 作为最大线程限制
                 * 因为当前任务已经进入队列,所以不设为第一个执行的任务,这个工作线程会去任务队列取任务执行
                 */
                addWorker(null, false);
            /*
             * 如果满足下面条件之一:
             * 1. 线程池不是运行状态,
             * 2. 线程池是运行状态,但是任务队列满了,
             * 创建工作线程,当前线程最第一个任务,用 maximumPoolSize 作为最大线程限制。
             */
        } else if (!addWorker(command, false))
            // 创建工作线程失败,执行拒绝策略
            reject(command);
    }

这里的代码短小精悍,有几个点先理解一下:

  • 线程池内部使用的是 CAS 方式来进行同步,并发环境下的执行期间,控制量 ctl 的值会被多次读取 ctl.get()
  • 线程池内的任务的最后有这几个命运,进入任务队列等待,创建线程并执行,或者被拒绝。
  • Worker 和 Thread 是一一对应的,workerCount 就是有效工作线程的数量。

作者的对整个过程的一些关键内容的注释摘抄在这里:

  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.

作者对执行的过程有给出完整的注释,根据我的理解和补充,分为三步:

  1. 如果当前运行的线程数小于核心线程数,会尝试启动一个新的线程,这里的任务将成为线程的第一个任务。调用 addWorker 会自动检查运行状态 runState 和线程数 workerCount,并且在无法再添加新线程的情况下 addWorker 返回 false。
  2. 如果任务可以成功加入队列(队列满的话就无法加入了),在创建一个新的线程,还会在进行一次检查。原因在于线程池在被多线程访问的情况下,从第一次检查到现在,很有可能已经有几个线程终止了,也有可能线程池被调用 shutdown 退出 RUNNING 状态。检查状态后,如果线程被停止了,会把任务从任务队列中回滚,如果线程池没有下城的话会启动新线程。
  3. 如果无法让任务加入队列,会再尝试去加入一个新的线程。如果创建线程又失败了,比如线程池已经满了,或者已经被执行 shutdown 而退出 RUNNING 状态,会拒绝掉任务。

处理方式

根据上面对整个代码的梳理和注释,要能正确地执行任务,需要考虑到的点挺多的,比如:

  • 线程池是否是运行状态?
  • 任务队列满了吗?
  • 线程数量是否小于核心线程数量(可以理解为最小线程数)?
  • 如果线程池非运行状态,任务已经入队列了,是否还有线程可以把任务执行完?

最后三种处理方式:

  • 创建线程执行。addWorker(command, true) 用来创建核心线程,addWorker(command, false) 用来创建普通线程,addWorker(null, false) 用来创建空线程。
  • 加入任务队列。workQueue.poll(command) ,这里是阻塞队列 BlockingQueue 调用 poll 方法来入列。
  • 执行拒绝策略,reject(command) ,默认采用 AbortPolicy 的处理,抛出 RejectedExecutionException 异常。

RUNNING 状态下的流程

为了便于理解,我们假设线程池始终是在运行状态,就会得到这样的流程图:

addWorker

从 execute 方法得知,创建和启动新线程由 addWorker 完成。这里会根据线程池的状态和提供的线程数量边界来决定是否要创建新线程。添加新线程成功会返回 true,否则返回 false。

addWorker 有两个参数:

  • Runnable firstTask,如果这个值不为 null,新创建的线程将会马上执行它;如果这个值为 null,这里会启动一个空线程,然后去任务队列中取任务。什么时候不为 null?当前有效工作线程数量小于 corePoolSize ,或者任务队列满了。
  • boolean core,用来选择线程数量的边界,如果为 true,使用 corePoolSize ;如果为 false,则使用 maximumPoolSize

addWorker 方法可以分成两个阶段来解读。第一阶段是通过自旋锁的方式,来增加工作线程数。第二部阶段则是创建并且启动工作线程。

阶段一:增加 workerCount

第一阶段会根据线程池的状态,来判断是否可以创建或者启动新的线程。如果可以的话,会先增加 workerCount。

第一阶段的源码和我的根据理解添加的注释如下:

    private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        for (; ; ) {
            // 读取控制量 c 和当前运行状态 rs
            int c = ctl.get();
            int rs = runStateOf(c);

            /*
             * 检查线程池状态:
             * 1. 如果已经是 TIDYING 或者 TERMINATED,不创建线程,返回失败;
             * 2. 如果是 SHUTDOWN 状态,首个任务为空,任务队列不为空,会继续创建线程;
             * 3. 如果是 SHUTDOWN 状态,首个任务不为空,不创建线程,返回失败;
             * 4. 如果是 SHUTDOWN 状态,首个任务为空,任务队列为空,不创建线程,返回失败;
             *
             * 所以在 SHUTDOWN 状态下,不会再创建线程首先去运行 firstTask,只会去创建线程把任务队列没执行完的任务执行完。
             */
            if (rs >= SHUTDOWN && !(rs == SHUTDOWN && firstTask == null && !workQueue.isEmpty()))
                return false;

            // 自旋锁,CAS 操作增加工作线程数
            for (; ; ) {
                // 获取有效工作线程数
                int wc = workerCountOf(c);
                /*
                 * 检查有效工作线程数量是否达到边界:
                 * 1. 如果有效工作线程数大于最大工作线程数 2^29-1,不创建线程,返回失败;
                 * 2. 判断是否达到最大线程限制,core 为 true 的时候为核心线程数 corePoolSize,false 为最大线程数 maximumPoolSize。
                 */
                if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;

                /*
                 * 增加工作线程数,CAS 操作:
                 * 增加失败说明已经有其他线程修改过了,进行自旋重试;
                 * 增加成功,跳出自旋,进入下一个环节,去创建新线程。
                 */
                if (compareAndIncrementWorkerCount(c))
                    break retry;

                // 重新获取控制量的值
                c = ctl.get();

                // 比较当前的状态,如果运行状态改变了,从 retry 段重新开始
                if (runStateOf(c) != rs)
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }
        ...
}

如果是 execute 调用了 addWorker 来创建线程,在不同状态下的情况如下:

  • 线程池处于 TIDYING 或 TERMINATED 状态。返回给 execute 后,最后会执行 reject(command) 方法来执行拒绝策略。
  • 线程池处于 SHUTDOWN 状态,首个任务不为空。说明外部打算创建新的线程来执行该任务,该状态已经不能去创建新线程来执行未入队列的任务,所以直接返回 false,并且 execute 最后也会执行 reject(command) 方法来执行拒绝策略。
  • 线程池处于 SHUTDOWN 状态,首个任务为空,工作队列为空。从 execute 进来,首个任务为空只有 addWorker(null, false) 操作,这个执行前任务已经加入工作队列了。所以这里工作队列为空说明任务已经被其他线程执行完了。那么回到 execute 后,就不做任何处理直接返回。
  • 线程池处于 RUNNING 状态,或者 SHUTDOWN 状态首个任务为空,工作队列不为空的情况下,还会判断一下工作线程的数量。线程数量大于线程的容量 CAPACITY,最后会被 reject(command) ;如果线程数限制边界为 corePoolSize ,说明在创建核心线程,返回创建失败说明核心线程数已满,回到 execute 还会按核心线程满的情况继续执行;如果线程限制边界为 maximumPoolSize ,说明线程数已经达到最大线程数限制,而从 execute 到执行这条语句,说明工作队列也满了,也是执行 reject(command)

如果通过了这些状态下的不同条件的限制,表示可以添加新线程,这里先增加 workerCount 的数量。从线程安全的保证中我们知道 workerCount 在并发情况下是靠 CAS 来保证安全的。所以这里使用 CAS 加上自旋锁,然后增加 ctrl 中 workerCount 的值。

for (;;) {
  ...
  if (compareAndIncrementWorkerCount) {
    break retry;
  }
  ...
}

阶段二:创建并运行新线程

第一阶段控制量的 ctl 中表示有效工作线程数的 workerCount 增加。但实际上工作线程还没有创建,这里进入第二阶段,创建并且启动工作线程。

第二步的源码和我添加的理解如下:

    private boolean addWorker(Runnable firstTask, boolean core) {
        ...

        // 标记值,表示工作线程已经启动,默认没有启动
        boolean workerStarted = false;
        // 标记值,表示工作线程已经添加,默认没有添加
        boolean workerAdded = false;
        Worker w = null;
        try {
            // 创建新的 worker,并且传入 firstTask
            w = new Worker(firstTask);
            final Thread t = w.thread;
            /*
             * 判断创建的线程是否为 null,线程是由 ThreadFactory 创建的;
             * 如果创建的线程为 null,后面会执行 addWorkerFailed(w)
             */
            if (t != null) {
                // 接下来要访问工作线程队列,它是 HashSet 类型,非线程安全集合需要加锁访问
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                    // Recheck while holding lock.
                    // Back out on ThreadFactory failure or if
                    // shut down before lock acquired.

                    // 重新获取工作线程数
                    int rs = runStateOf(ctl.get());

                    /*
                     * 再次判断线程池状态是否满足下面的条件之一:
                     * 1. 处于 RUNNING 状态
                     * 2. 处于 SHUTDOWN 状态,但是首任务为空。这里开线程来跑任务队列的剩余任务。
                     */
                    if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) {
                        /*
                         * 判断新创建的线程状态,如果线程已经是启动后的状态,就无法启动执行任务;
                         * 这个是个不可恢复的异常,会抛出 IllegalThreadStateException 异常。
                         */
                        if (t.isAlive()) // precheck that t is startable
                            throw new IllegalThreadStateException();

                        // 工作线程进入工作线程队列,并且更新这个队列达到过的最大线程数
                        workers.add(w);
                        int s = workers.size();
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                // 根据标记值得知线程已经成功创建,启动线程,更新标记 workerStarted 表示线程已经启动。
                if (workerAdded) {
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            /*
             * 有这种情况,线程没有启动
             * 1. ThreadFactory 返回的线程为 null
             * 2. 线程池进入了 SHUTDOWN 状态,并且首任务不为空。
             * 这是因为这段代码执行期间线程状态发生了改变,比如 RUNNING 的时候进来,
             * 准备创建核心线程的时候,线程池被关闭了,这个任务就不会执行。
             * 所以即使是在创建核心线程的时候调用了 shutdown,任务也是不执行的。
             * 3. ThreadFactory 返回的线程已经被启动了,抛出 IllegalThreadStateException 异常
             */
            if (!workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }

如果创建并且启动线程成功后,会调用 t.start() 启动线程。

因为 w = new Worker(firstTask); 创建 Worker 实例的时候,会把 Worker(也实现了 Runnable)作为 Thread 的 Runnable。

Worker(Runnable firstTask) {
    setState(-1); // inhibit interrupts until runWorker
    this.firstTask = firstTask;
    this.thread = getThreadFactory().newThread(this);
}

所以 t.start() 后,会调用 Worker 实现好的 run 方法

/**
 * Delegates main run loop to outer runWorker.
 */
public void run() {
    runWorker(this);
}

这里是代理了 ThreadPoolExecutor 的 runWorker 的方法。后面将会在这个 runWorker 让线程从任务队列不断地取任务执行。

如果创建线程失败了,会执行 addWorkerFailed 方法。所以执行 addWorkerFailed,都是一些突然发生的问题:

  • 线程的异常,ThreadFactory 返回了 null 的线程,或者线程已经被启动过抛出的 IllegalThreadStateException 异常。
  • 状态的变化,创建线程期间线程池被关闭了,进入关闭和终止流程。

addWorkerFailed

做一些线程创建失败的善后工作。

private void addWorkerFailed(Worker w) {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
    if (w != null)
        workers.remove(w);
    decrementWorkerCount();
    tryTerminate();
    } finally {
        mainLock.unlock();
    }
}

如果已经在工作线程队列,移除工作队列。

在创建线程前工作线程数量有增加,这里做个减法恢复。调用 decrementWorkerCount ,这个还是使用自旋锁的修改过程:

private void decrementWorkerCount() {
    do {
    } while (!compareAndDecrementWorkerCount(ctl.get()));
}

然后尝试进入到 TERMINATED 状态。

作者的说明大意为,如果线程池正在关闭,这个 Worker 可能会停止了线程池的终止过程,所以这里尝试再调用tryTerminate 重新尝试终止。

runWorker

再回到之前的 addWorker,最后面调用了 t.start() 后,最后会启动线程池的 runWorker 方法。

runWorker 方法会启动一个死循环,让线程不断地从任务队列中取任务执行。如果线程被中断结束或者因为异常结束后,还调会用了 processWorkerExit 处理线程退出

代码和我理解后添加的注释如下:

    final void runWorker(Worker w) {
        // 获取当前线程,其实和 Worker 内的线程一致
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts

        // 标记变量,用来记录线程执行期间是否因为异常终止
        boolean completedAbruptly = true;
        try {
            /*
             * 循环获取任务:
             * 1. 如果第一个任务存在并且还没有执行,使用第一个任务;
             * 2. 从任务队列中读取获取任务
             */
            while (task != null || (task = getTask()) != null) {
                // 加锁,表示该线程已经在执行任务了。
                w.lock();

                /*
                 * 这里中断标记的处理:
                 * 1. 如果线程池运行状态是 STOP 状态,设置中断;
                 * 2. 条件一不是 STOP 状态,进入条件二。先调用 interrupted 清除中断标记。
                 * 清除前如果不是中断状态,不设置中断;
                 * 3. 清除前是中断状态,因为条件判断执行期间,线程池可能调用了 shutdownNow 方法,
                 * 所以再判断一下运行状态。如果这时候是 STOP 状态,并且之前设置的中断已经清了,
                 * 这时候要恢复设置中断。
                 */
                if ((runStateAtLeast(ctl.get(), STOP) || (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP)))
                        && !wt.isInterrupted())
                    wt.interrupt();
                try {
                    // 钩子方法,执行任务前
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                        // 运行任务,
                        task.run();
                    } catch (RuntimeException x) {
                        thrown = x;
                        throw x;
                    } catch (Error x) {
                        thrown = x;
                        throw x;
                    } catch (Throwable x) {
                        thrown = x;
                        throw new Error(x);
                    } finally {
                        // 钩子方法,执行任务后。因为放在 finally 块中,出现异常也会执行该钩子方法。
                        afterExecute(task, thrown);
                    }
                } finally {
                    task = null;
                    w.completedTasks++;
                    // 释放锁
                    w.unlock();
                }
            }
            // 异常终止标记位设为 false,表示执行期间没有因为异常终止
            completedAbruptly = false;
        } finally {
            // 线程退出处理
            processWorkerExit(w, completedAbruptly);
        }
    }

为了便于线程池使用者监控线程池的状态,每个线程执行任务前和执行任务后都会执行两个钩子方法,分别为:

  • beforeExecute
  • afterExecute

需要监控线程池的,继承 ThreadPoolExecutor 然后实现这两个方法即可。

getTask

从任务队列中取任务给线程执行,空闲线程的超时处理也在这里。

源码和我理解后的注释如下:

   private Runnable getTask() {
        // 标记变量,标记 poll 方法是否超时
        boolean timedOut = false;

        for (; ; ) {
            // 获取控制量的值 c 和当前线程池状态 rs
            int c = ctl.get();
            int rs = runStateOf(c);

            /*
             * 判断线程状态:
             * 1. 如果是 RUNNING 状态,进入下一步;
             * 1. 如果是 SHUTDOWN 状态,并且任务队列为空,返回 null,减少 workerCount;
             * 2. 如果是 STOP,TIDYING 或者 TERMINATED 状态,直接返回 false,减少 workerCount。
             */
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                decrementWorkerCount();
                return null;
            }

            // 获取有效线程数
            int wc = workerCountOf(c);

            /*
             * 标记变量,表示当前 worker 是否要超时退出
             * 1. allowCoreThreadTimeOut 设置 true 的话,所有的线程都要超时退出;
             * 2. 否则,只有当有效线程数大于核心线程数,需要减少线程池的数量,要设置超时退出。
             */
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

            // 根据是否超时、线程数大小、任务队列大小的情况,判断是否要退出
            if ((wc > maximumPoolSize || (timed && timedOut)) && (wc > 1 || workQueue.isEmpty())) {
                // 如果可以减少 workerCount,返回 null,否则进入自旋,进入下一个循环。
                if (compareAndDecrementWorkerCount(c))
                    return null;
                continue;
            }

            try {
                /*
                 * 如果要超时退出,当前使用 poll 方法,并设置了超时时间,超时后会退出
                 * 如果不需要设置超时,使用 take 方法,一直阻塞直到队列中有任务
                 */
                Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take();
                if (r != null)
                    return r;
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }

在 runWorker 中,如果有首先执行的任务,是不会调用 getTask 的。之后,都会从这里取任务执行。

如果线程取不到任务,返回 null 的同时,会调用 decrementWorkerCount 减少有效工作线程数量 workerCount。

什么时候会取不到任务返回 null 让线程关闭呢?作者的方法注释中总结如下:

  • 当前线程数大于最大线程数 maximumPoolSize ,比如执行期间调用 setMaximumPoolSize 方法调整了配置。
  • 线程池进入了 STOP 状态。我们从前面线程池的生命周期可以知道,SHUTDOWN 状态下,如果任务队列还有任务没有执行完,会先执行完。而 STOP 状态更严格,不会等待队列里的任务执行完就会退出线程。
  • 线程池进入了 SHUTDOWN 状态,并且任务队列为空。SHUTDOWN 状态下,如果任务队列没有任务了,线程不会阻塞等待任务,getTask 返回 null 让线程退出。
  • 如果线程池设置了超时时间,并且满足这两种情况之一:1. allowCoreThreadTimeOut 被设置为 true,说明所有的线程超时都要关闭。 2. 当前工作线程数量大于 corePoolSize 。就会使用 BlockingQueue 的 poll 方法设置超时时间去取任务。超时时间到了取不到任务后,还会进入下一个循环,再做一次判断 (timed && timedOut)) && (wc > 1 || workQueue.isEmpty()) ,确定线程不是最后一个线程,并且队列为空。也就是说,如果超时后发现队列还有任务,那么还会再次尝试去取任务;超时后发现整个线程池只剩下一个线程的话,那么这个线程会留着不会关闭。

我们从上面的 runWorker 解析可以看到,当 getTask 返回 null 的话,runWorker 的循环会被打破,然后进入线程退出处理 processWorkerExit。

所以线程进入退出流程的重要条件,就是 getTask 返回了 null。

processWorkerExit

如何处理工作线程的退出?这里有个 processWorkerExit。这个方法有两个参数:

  • Worker w ,要执行关闭的 Worker 对象。线程池会取出 Worker 里对 Thread 执行的一些记录,比如完成的任务数 completedTasks。
  • boolean completedAbruptly ,是否是异常引起的关闭。如果是的话,线程池会尝试再开启一个线程来做补充。

源码和理解后的注释如下:

    private void processWorkerExit(Worker w, boolean completedAbruptly) {
        // 如果是异常退出的话,workerCount 还未调整,这里做个减法
        if (completedAbruptly)
            decrementWorkerCount();

        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            // 把之前 Worker 的完成任务数收集起来,然后从工作线程队列中移除。
            completedTaskCount += w.completedTasks;
            workers.remove(w);
        } finally {
            mainLock.unlock();
        }

        tryTerminate();

        int c = ctl.get();
        if (runStateLessThan(c, STOP)) {
            /*
             * 如果还在 RUNNING 或者 SHUTDOWN 状态,这里还处理
             * 1. 如果是异常退出,再启动一个新的线程替换。
             * 2. 如果不是异常退出,确定一个最少存在的线程数:
             * 如果设置了 allowCoreThreadTimeout 的话,并且任务队列还有值,min = 1,
             * 如果设置了 allowCoreThreadTimeout 的话,min = corePoolSize,
             * 然后如果当前工作线程数比 min 小的话,会再启动一个新线程替换。
             */
            if (!completedAbruptly) {
                int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
                if (min == 0 && !workQueue.isEmpty())
                    min = 1;
                if (workerCountOf(c) >= min)
                    return; // replacement not needed
            }
            addWorker(null, false);
        }
    }

所以最后 processWorkerExit 做了这几件事情:

  • 异常退出线程的话,不是走 getTask 返回 null 退出,所以没有减少 workerCount,这里会再调用 decrementWorkerCount 减一下 workerCount。
  • 把 Worker 里面记录的已经完成的任务数收集到 completedTaskCount 中。
  • 把 Worker 从工作线程队列 workers 中移除。
  • 也会调用一下 tryTerminate,这个我们在之前的 addWorkerFailed 也看到有调用。原因一样,还是这个 Worker 可能停止了线程池的终止过程,所以这里尝试再调用tryTerminate 重新尝试终止线程池。
  • 如果线程池是 RUNNING 或者 SHUTDOWN 状态,并且是异常退出的话,会在启动一个新线程替换;不是异常退出的话,根据是否配置 allowCoreThreadTimeout 来会确定一个 min 的值。如果设置了 allowCoreThreadTimeout = true,所有的线程超时都要退出,就没有核心线程的概念了,所以 min = 0。如果 allowCoreThreadTimeout = false,那就有核心线程数的概念,这是要常驻的线程,min = corePoolSize。 然后再看当前线程池的数量和任务队列的数量,如果任务队列不是空的,还有队列,至少需要一个线程把任务执行完,如果 min 为 0 调整为 1,确保有线程把任务队列中尚未执行完的任务执行完。

状态获取

当线程池已经启动,内部的状态也提供了接口返回。这些返回的数据可以用来对整个线程池的总体运行情况进行监控。

getPoolSize

因为访问了工作线程队列,所以使用 mainLock 进行独占式加锁,源码如下:

public int getPoolSize() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        // Remove rare and surprising possibility of
        // isTerminated() && getPoolSize() > 0
        return runStateAtLeast(ctl.get(), TIDYING) ? 0 : workers.size();
    } finally {
        mainLock.unlock();
    }
}

可以看到,如果已经进入了 TIDYING 状态,会直接返回 0。

getActiveCount

获取正在执行任务的线程数。如何知道线程正在执行任务?

根据我们之前的分析,ThreadPoolExecutor 的 Worker 自己继承 AQS 实现了不可重入锁,然后在 runWorker 的时候,每次获取到 Runnable 要执行前都会调用 w.lock() 加锁。所以,只要遍历一下工作线程队列 workers 每一个 Worker 是否被加锁了,就可以知道是否在执行 Runnable。

因为访问了工作线程队列,所以使用 mainLock 进行独占式加锁,源码如下:

public int getActiveCount() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
    int n = 0;
    for (Worker w : workers)
        if (w.isLocked())
            ++n;
        return n;
    } finally {
        mainLock.unlock();
    }
}

这里再次感受到 Worker 实现非重入锁来表示线程是否正在运行的方式的强大。

getTaskCount

获取线程池执行的任务数,包括已经执行的和正在执行的。

在第二章中,我们知道 Worker 除了维持了控制中断状态,还有记录功能,内部有一个变量 completedTasks 来记录线程已经完成的任务数,在 runWorker 的方法中,每执行完一个任务会增加计数。

while (task != null || (task = getTask()) != null) {
    ...
    try {
        ...
    } finally {
        task = null;
        w.completedTasks++;
        ...
    }
}

并且在线程超时退出或者某些异常退出时,会执行 processWorkerExit,内部会把退出线程执行的任务数收集到变量 completeTaskCount 中

completedTaskCount += w.completedTasks;

所以在 getTaskCount 只需要把它们都加起来。又因为需要统计正在执行的任务数,我们通过之前的分析,w.isLocked 表示线程正在执行任务,所以还有再 +1。源码如下:

public long getTaskCount() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
    long n = completedTaskCount;
    for (Worker w : workers) {
        n += w.completedTasks;
        if (w.isLocked())
            ++n;
        }
        return n + workQueue.size();
    } finally {
        mainLock.unlock();
    }
}

还有,作者注释也有说明,这里只是一个大概值,一个完成任务数的快照。因为我们在执行这个代码期间,并不会阻塞线程池执行任务,所以很可能又执行了多个任务。

关闭操作

如果线程池不需要使用了,外界可以调用一些方法关闭线程池。比如应用某个模块使用的线程池,离开这个模块后,又长时间不回去调用,可以考虑把线程池关了,否则如果线程池常驻着几个核心线程的话也是挺浪费资源的。

比如在已经没有新的任务需要执行的时候去执行 shutdown。

shutdown

该方法会触发线程池关闭流程,但不会马上关闭,所以该方法是没有返回值的。调用该方法后什么时候才能知道线程池已经被关闭了?需要使用 awaitTermination 去判断。

shutdown 的源码如下:

public void shutdown() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        advanceRunState(SHUTDOWN);
        interruptIdleWorkers();
        onShutdown(); // hook for ScheduledThreadPoolExecutor
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
}

首先会执行 checkShutdownAccess,这个用来确认调用者是否有权限修改线程状态。如果没有权限的话会抛出 SecurityException。

接着调用 advanceRunState 来改变状态,把 runState 修改为 SHUTDOWN。内部使用 CAS 加自旋,直到状态称为 SHUTDOWN:

for (; ; ) {
    int c = ctl.get();
    if (runStateAtLeast(c, targetState) || ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c))))
        break;
}

状态设置完了,调用 interruptIdleWorkers 方法去中断掉那些空闲的线程。

然后留了一个钩子方法 onShutDown,如果有需要的话,可以继承 ThreadPoolExecutor 实现该回调,用来监控线程池的状态。之前我们分析 runWoker 的时候,也有两个钩子方法 beforeExecute 和 afterExecute。

最后会调用 tryTerminate 方法,尝试终止线程池。当然,这里只是尝试一下,如果任务队列还有任务没有执行完,这个尝试是不成功的。

shutdownNow

这个方法和 shutdown 类似,但会去关闭所有线程,不会再去执行任务队列中的未执行的任务,把这些未执行的队列返回。源码如下:

public List<Runnable> shutdownNow() {
    List<Runnable> tasks;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        advanceRunState(STOP);
        interruptWorkers();
        tasks = drainQueue();
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
    return tasks;
}

可以看到,和 shutdown 基本相同,但是把状态通过 advanceRunState 修改为 STOP。我们之前线程取任务执行的 getTask 方法中,有对 STOP 状态的判断:

if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
    decrementWorkerCount();
    return null;
}

可以看到,只要是大于等于 STOP 状态,即便 workQueue 还有任务也不返回了,直接返回 null 通知线程关闭。

如果有一些线程不是空闲线程,比如正在 sleep 或者 wait 的线程,是会调用中断方法线程中断方法去中断这些线程的。还有设计运行任务 Runnable 的时候,如果因为业务场景一定需要如果一个死循环跑任务的话,最后加一个中断状态的判断,否则这些线程永远关不掉,线程池也无法正常终止。不过这种任务出现在线程池是一个设计上的失误。

比如运行工作线程的 runWorker 有对 STOP 状态的处理,没有设置中断会进行中断的设置:

if ((runStateAtLeast(ctl.get(), STOP) || (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP)))
        && !wt.isInterrupted())
    wt.interrupt();

接着会调用 drainQueue 方法把任务队列 workQueue 的任务排出,放入 tasks 后返回出来。

最后会调用 tryTerminate 方法,尝试终止线程池。

tryTerminate

前面分析的 addWorkerFailed,processWorkerExit,shutdown,shutdownNow 都有调用 tryTerminate 方法。源码如下:

    final void tryTerminate() {
        for (; ; ) {
            int c = ctl.get();
            if (isRunning(c) || runStateAtLeast(c, TIDYING) || (runStateOf(c) == SHUTDOWN && !workQueue.isEmpty()))
                return;
            if (workerCountOf(c) != 0) { // Eligible to terminate
                interruptIdleWorkers(ONLY_ONE);
                return;
            }

            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
                    try {
                        terminated();
                    } finally {
                        ctl.set(ctlOf(TERMINATED, 0));
                        termination.signalAll();
                    }
                    return;
                }
            } finally {
                mainLock.unlock();
            }
            // else retry on failed CAS
        }
    }

可以看到这个方法就是尝试去终止线程,让线程池进入最后的两个生命周期 TIDYING 和 TERMINATED,并且做最后的收尾操作。

当然,这里只是尝试,对照代码我们可以知道,这些情况下不会直接退出尝试。

  • 当前状态是 RUNNING,不终止

  • 当前状态已经是 TIDYING 或者 TERMINATED 状态,不需要重复终止,直接退出。

  • 当前装填是 SHUTDOWN,但是任务队列 workQueue 里还有任务,不终止,需要把任务执行完。

  • 如果上面的条件都通过了,判断终止的资格。我们之前分析过线程池的生命周期,要进入 TIDYING 和 TERMINATED 状态,必须工作线程都关闭,workerCount 为 0。所以这里做了了一个判断:

    if (workerCountOf(c) != 0) { // Eligible to terminate
    interruptIdleWorkers(ONLY_ONE);
    return;
    }

    如果还有线程未关闭,调用 interruptIdleWorkers(ONLY_ONE) 去关闭。这里采用一个一个线程去关闭,原因是确保包括在 SHUTDOWN 阶段启动的,用来保证任务队列任务执行完的那个线程也能及时的关闭。

线程都关闭了,任务也都执行完了,通过 CAS 的方式改变状态后,线程池会再调用一次钩子方法 terminated

最后直接设置状态为 TERMINATED,因为执行到这里已经没有多线程竞争了,可以直接设置。ctl.set(ctlOf(TERMINATED, 0));

mainLock 的 Condition 的实例 temination 发出信号,通知 awaitTermination 线程池正式终止。

awaitTermination

如之前所说,外部调用了 shutdown 或者 shutdownNow,线程池还要进行一些收尾工作,不会马上终止。

这里提供了一个方法 awaitTermination,阻塞等待线程池终止。方式通过一个循环,不断地去判断线程池是否进入 TEMINATED 状态。

public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
    long nanos = unit.toNanos(timeout);
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        while (!runStateAtLeast(ctl.get(), TERMINATED)) {
            if (nanos <= 0L)
                return false;
            nanos = termination.awaitNanos(nanos);
        }
        return true;
    } finally {
        mainLock.unlock();
    }
}

如果不停地循环去获取状态,那对 CPU 很残忍,会消耗大量资源。所以作者使用了 ReentrantLock 和 Condition 的方式,之前 tryTermindate 结束后会调用 termination.signalAll() ,这里再被唤醒进入下一个循环读取状态。这里还有超时的设置。

interrupIdleWorkers

这个方法用来关闭空闲的线程。

比如线程池运行期间调用 setMaxmiumPoolSize 等方法会调用这个方法清理一下线程池,shutdown 关闭线程池的方法也会执行。这个方法最终调用 interruptIdleWorkers(false)

具体的源码如下:

private void interruptIdleWorkers(boolean onlyOne) {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        for (Worker w : workers) {
            Thread t = w.thread;
            if (!t.isInterrupted() && w.tryLock()) {
                try {
                    t.interrupt();
                } catch (SecurityException ignore) {
                } finally {
                    w.unlock();
                }
            }
            if (onlyOne)
                break;
            }
    } finally {
        mainLock.unlock();
    }
}

这里逐个去调用 workers 中的每个 Worker,取出他们的 Thread,判断如果还没有设置中断,并且没有执行任务,就会去调用 interrupted 方法设置中断。

像我们之前说的,Worker 执行任务的时候会加自己内部实现的不可重入锁,所以这里 w.tryLock() 能拿到锁的话,该线程已经进入空闲状态。

空闲的线程如果在 getTask 阶段,调用了 workQueue.take() 在阻塞读取任务队列中的任务,是否可以被中断掉?

我们看下同步队里 BlockQueue 中对 take 的定义:

E take() throws InterruptedException;

所以该接口声明了 take 方法是会响应线程中断的。

实现方是怎么响应的?我们看一下 LinkedBlockingQueue 的实现:

public E take() throws InterruptedException {
    E x;
    ...
    takeLock.lockInterruptibly();
    ...
    return x;
}

调用了 ReentrantLock 的 lockInterruptibly 方法来加锁。

public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
}

ReentrantLock 的加锁是 AQS 的实现。我们跟踪 AQS 实例 sync 的acquireInterruptibly 方法:

public final void acquireInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (!tryAcquire(arg))
        doAcquireInterruptibly(arg);
}

可以看到,有对线程中断标记的判断,如果中断后就抛出 InterruptedException 异常。这样子使用 take 阻塞等待的线程就可以把异常抛出,最后调用 processWorkerExit 成功退出。

猜你喜欢

转载自blog.csdn.net/firefile/article/details/80514627