【深度长文】深入分析Java线程池——ThreadPoolExecutor

Java 线程池概述

Java 语言中创建线程看上去就像创建一个对象,仅仅调用 new Thread() 即可,但实际上创建线程比创建对象复杂得多。创建对象仅仅是在 JVM 的堆里分配一块内存,而创建线程,需要 调用操作系统内核的 API,然后操作系统要为线程分配一系列的资源,这个成本很高。因此,线程是一个重量级的对象,应该避免频繁的创建和销毁。


线程池没有采用一般的池化资源设计方法(例如:连接池、对象池),因为我们无法获取一个启动的 Thread 对象,然后动态地将需要执行的任务 Runnable task 提交给线程执行。目前业界地线程池设计,普遍采用生产者-消费者模型,线程池的使用方为 Producer,而线程池中的 工作线程为 Consumer

ThreadPoolExecutor 构造方法

Java 提供的线程池相关的工具类中,最核心的是ThreadPoolExecutor。ThreadPoolExecutor 的构造函数如下:

  • corePoolSize:线程池保有的最小线程数(核心线程数)。如果线程池中的线程数小于 corePoolSize,提交任务时会创建一个核心线程,该任务作为新创建的核心线程第一个执行的任务。
  • maximumPoolSize:最大线程数。如果提交任务时任务队列已经满了,且当前工作线程数小于 maximumPoolSize,会创建新的工作线程用于执行该任务;反之如果工作线程数大于等于 maximumPoolSize,则执行拒绝策略。
  • keepAliveTime & unit:一个线程如果在时间单位为 unit 的 keepAliveTime 时间内没有执行任务,而且线程池的线程数大于 corePoolSize ,那么这个空闲的线程就要被回收。
  • workQueue:任务队列,为 BlockingQueue 实现类。
  • threadFactory:线程工厂,通过该参数可以自定义如何创建线程。ThreadFactory 是一个接口,里面是有一个 newThread 方法等待实现:
    Thread newThread(Runnable r);//接口方法默认为public abstract
    
  • handler:任务的拒绝策略,如果线程池中 所有的线程都在忙碌,且任务队列已经满了(前提是任务队列是有界队列),此时提交任务线程池会拒绝执行。决绝的策略可以通过该参数指定。

温馨提示:线程池的静态工厂类 Executors 提供了很多开箱即用的线程池,可以帮助快速创建线程池,但提供的线程池很多使用的是 无界队列 LinkedBlockingQueue,无界队列很容易导致 OOM,而 OOM 会导致所有请求都无法处理。
在阅读完本节后我们知道,在生产环境中使用线程池时需要设置 ThreadPoolExecutor 构造方法的 workQueue 参数为 ArrayBlockingQueue 等有界阻塞队列。


线程池拒绝策略

上一小节提到,构造方法中的 RejectedExecutionHandler handler 参数可以用于自定义任务拒绝策略。ThreadPoolExecutor 已经提供了 4 种拒绝策略:

  • CallerRunsPolicy:提交任务的线程自己去执行该任务。

  • AbortPolicy:默认的拒绝策略,会抛出 RejectedExecutionException。

  • DiscardPolicy:直接丢弃任务,没有任何异常抛出。

  • DiscardOldestPolicy:丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列。


默认拒绝策略为 AbortPolicy,该拒绝策略抛出 RejectedExecutionException 为运行时异常,编译器不会强制 catch,开发人员可能会忽略,因此默认拒绝策略要慎重使用
如果线程池处理的任务非常重要,建议自定义自己的拒绝策略;并且在实际工作中, 自定义的拒绝策略往往和 降级策略 配合使用

例如:将任务信息插入数据库或者消息队列,配置 XXL-JOB 定时任务扫描失败任务表,将执行失败的任务交给专用于补偿的线程池去进行补偿


工作流程

线程池中有几个重要的概念:核心线程池(CorePool)、**空闲线程池(IdlePool)**以及 任务队列。下图为我绘制的线程池工作流程图,包含上述三个概念模型,cpSize 核心线程池中当前的线程数、cpCap 核心线程池容量、ipSize 空闲线程池中当前线程数。

请添加图片描述

我来简述下提交任务 task 时,线程池的执行流程:

  1. 如果核心线程池未满,即 cpSize 小于 cpCap,通过线程工厂 创建一个核心线程,将 task 作为新线程的第一个任务。

  2. 如果 核心线程池已满,但是任务队列仍然有空间,将 task 添加到任务队列。核心线程在执行完手头的任务后,会从任务队列中获取新的任务,继续执行。如果任务队列为空,核心线程会阻塞在任务获取阶段,直到有 新的任务提交到任务队列

  3. 如果任务队列已满,则创建空闲线程,并将 task 作为第一个执行的任务。空闲线程如果执行完手头的任务,也会从任务队列中获取新的任务。
    如果任务队列为空,空闲线程会阻塞,直到 超出 keepalive 设定的时间 或 获取到新的任务执行。如果等待新任务超时,空闲线程的生命周期就会结束了。

  4. 如果空闲线程数+核心线程数已经达到了 maximumPoolSize,创建新线程的方法会失败,此时提交的任务将被拒绝,拒绝策略由 RejectedHandler 负责执行。


并发库中的线程池

java.util.concurrent.Executors 提供了通用线程池创建方法,去创建不同配置的线程池,该工具类目前提供了五种不同的线程池创建配置:

CachedThreadPool

CachedThreadPool 是一种用来 处理大量短时间工作任务的线程池,会在先前构建的线程可用时重用已创建的工作线程,但是当工作线程空闲超过 60s,则会从线程池中移除。

任务队列为 SynchronousQueue,它是一个不存储元素的阻塞队列(容量 0),提交任务的操作必须等待工作线程的移除操作,反之亦然。

在这里插入图片描述


为什么使用 SynchronousQueue 作为任务队列

个人想法:线程池的工作逻辑是,提交任务时如果 核心线程数达到 corePoolSize 且任务队列已满,则会创建空闲线程执行。因为 SynchronousQueue 容量为 0 天然是满的,且 corePoolSize 被设置为 0,这意味着创建任务时如果没有可用线程,就会立即创建一个新线程来处理任务。

这使得 CachedThreadPool 在执行大量短期异步任务时更加高效,避免了任务对线程资源的等待,符合设计初衷:快速执行大量的短暂任务

FixedThreadPool

核心线程数和最大线程数相等,使用的是 无界任务队列 LinkedBlockingQueue。如果当前的工作线程数已经达到 nThreads,任务将被添加到任务队列中等待执行。如果有工作线程退出,下一次提交任务时将会有新的工作线程被创建来补足线程池。

SingleThreadExecutor

工作线程限制为 1 的 FixedThreadExecutor,它 保证了所有任务的都是被顺序执行

ScheduledThreadPool

ScheduledThreadPoolExecutor 允许安排一个任务在延迟指定时间后 执行,还可以 周期性地执行任务。周期性调度任务有两种类型:固定延迟和固定频率。固定延迟 是在上一个任务结束和下一个任务开始之间保持固定的延迟,而 固定频率 是以固定的频率执行任务,不管任务的执行时间多长。


ScheduledThreadPoolExecutor 中定义了内部类 DelayedWorkQueue 作为任务队列,DelayedWorkQueue 是基于堆的数据结构。队列中的元素为 RunnableScheduledFuture 类型:

private RunnableScheduledFuture<?>[] queue = new RunnableScheduledFuture<?>[INITIAL_CAPACITY];

RunnableScheduledFuture 接口继承关系如下图所示:

  • Delayed 接口继承了 Comparable 接口,getDelay 方法返回任务剩余的延迟时间,返回值小于等于 0 说明延迟的时间已过;compareTo 方法用于比较任务下一次的执行时间,用于维护小顶堆属性(父节点任务的执行时间小于儿子节点)。
  • RunnableFuture 接口的 run 方法定义了需要执行的任务逻辑,Future 接口用于获取异步任务的执行结果。

DelayedWorkQueue#take 方法用于获取下一个需要执行的定时任务,代码及详细注释如下:

public RunnableScheduledFuture<?> take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    // 上锁, 避免堆数据访问产生的数据竞争
    lock.lockInterruptibly();
    try {
        for (;;) {
            // 堆顶元素: Delayed#getDelay 延迟时间最小的任务
            RunnableScheduledFuture<?> first = queue[0];
            if (first == null)
                // 堆中任务空, 等待新任务入堆
                available.await();
            else {
                long delay = first.getDelay(NANOSECONDS);
                if (delay <= 0)
                    // delay小于等于0, 说明延迟时间已过, 可以执行;
                    // finishPoll 弹出堆顶任务
                    return finishPoll(first);
                first = null; // don't retain ref while waiting
                if (leader != null)
                    // leader为等待堆顶任务到达执行时间的线程
                    // leader 非空说明已经有线程正在等待堆顶任务可执行, 因此当前线程为 follower, 需要等待直到堆顶元素变更
                    available.await();
                else {
                    // 当前线程是等待堆顶元素的 leader 线程, 设置 leader 属性
                    Thread thisThread = Thread.currentThread();
                    leader = thisThread;
                    try {
                        // 等待任务延迟的时间
                        available.awaitNanos(delay);
                    } finally {
                        // await期间会释放锁, leader可能因为新任务的加入而失效(当前线程可能等待的不再是堆顶任务)
                        // 所以await超时后, 需要判断leader是否为当前线程, 为当前线程才能设为null
                        if (leader == thisThread)
                            leader = null;
                    }
                }
            }
        }
    } finally {
        // 任务队列非空, leader为空说明没有线程等待堆顶元素可执行, 此时唤醒 follower 线程, 尝试获取堆顶的任务
        if (leader == null && queue[0] != null)
            available.signal();
        lock.unlock();
    }
}

ThreadPoolExecutor 源码分析

线程池状态表示

ThreadPoolExecutor 最重要的状态参数为:线程池状态(rs) 以及 活跃线程数(wc)。ThreadPoolExecutor 使用一个 Integer 变量 ctl 存储这两个状态参数:

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

Integer 长度位 32 bits,ctl 中最高的三位 (29-31) 存储线程池状态,低 29 位 (0~28) 存储活跃线程数,因此线程池中活跃线程数理论上限为 2 29 − 1 2^{29}-1 2291

了解了 ThreadPoolExecutor 的这种设计之后,我们来看看状态相关的位运算:

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

// runState is stored in the high-order bits
private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;

CAPACITY 表示线程池中活跃线程的理论上限 2 29 − 1 2^{29}-1 2291COUNT_BITS 表示线程数位数( 32 − 3 = 29 32-3=29 323=29)。

RUNNING 、SHUTDOWN、STOP、TIDYING、TERMINATED 为线程池的五种状态。根据代码,这五种状态表示如下图所示:

在这里插入图片描述

  • RUNNING:可接收新的任务,并且处理队列中排队的任务;
  • SHUTDOWN:不接收新的任务,但会处理队列中剩下的任务;
  • STOP:不接收新任务,不处理队列中的任务,并且中断进行中的任务;
  • TIDYING:所有的任务终止,工作线程数 (workerCount) 等于 0;
  • TERMINATED:线程池关闭,terminated() 方法完成。

获取 runState

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

runStateOf 方法从 ctl 获取线程池运行状态,保留 ctl 的最高的三位,其余位设置为 0。以 STOP 状态、3 个活跃线程数的 ctl 为例,求 rs 的过程如下:
在这里插入图片描述


获取 workerCount

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

workerCountOf 获取线程池中的活跃线程数,即保留 ctl 的 0-28 位,将 29-31 位设置为 0。

在这里插入图片描述

生成 ctl

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

ctlOf 通过状态值和线程数值计算出 ctl,就是对 rs 和 wc 进行或运算,保留 wc 的 0-28 位和 rs 的 29-31 位。

提交任务 execute()

ThreadPoolExecutor#execute 方法用于提交任务给线程池执行,代码以及详细注释如下:

public void execute(Runnable command) {
     if (command == null)
         throw new NullPointerException();

     // 获取线程池状态参数 ctl
     int c = ctl.get();
     // 如果活跃线程数小于核心线程池容量corePoolSize, addWorker创建新线程, 以command作为第一个任务
     if (workerCountOf(c) < corePoolSize) {
         if (addWorker(command, true))
             return;
         // 创建新线程失败, 更新 ctl
         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);
     }
     // 任务添加到队列失败, (1)线程池状态不是RUNNING状态 或 (2)任务队列已满
     // 尝试增加非核心线程, 执行 command 任务, 如果线程池不为RUNNING, addWorker会返回false
     else if (!addWorker(command, false))
         // 线程池不为RUNNING, 新增非核心线程失败, 执行任务拒绝策略
         reject(command);
 }

这段代码的主要逻辑很简洁:

  1. 当 wc 小于 corePoolSize 时,创建核心线程执行 command 任务;
  2. 如果核心线程数已满,则将任务缓存在任务队列中 (workQueue.offer),工作线程完成手头上的任务后,从任务队列中获取新任务。
  3. 如果任务队列也满了,offer 方法返回 false,尝试增加非核心线程执行 command。如果线程创建失败,reject 执行任务拒绝策略。

除此之外,我想在本篇博客中探讨下 execute 方法的一些实现细节,并给出我自己的观点用于抛砖引玉。


为什么需要二次检查

大家请看下面这段有关二次检查的代码,在阅读源码时,我产生了疑问: 为什么需要二次检查 ?该操作 解决了什么场景下的数据竞争

// ...
c = ctl.get(); // -------(1)
if (isRunning(c) 
		&& workQueue.offer(command)) { // -------(2)
    int recheck = ctl.get();  // -------(3)
    if (!isRunning(recheck) && remove(command))
        reject(command);
    else if (workerCountOf(recheck) == 0)
        addWorker(null, false);
}
// ...

执行二次检查的前提是:

  • 线程池在执行语句 (1) 的时候,是运行状态;
  • 任务队列未满,command 被添加至队列,(2) 处的 offer 方法返回 true;

假设没有二次检查,会发生什么

场景 1:在语句 (1) 后,语句(2) 执行前,线程池使用者调用了 shutdownNow 方法将线程池工作线程关闭,清空任务队列中的任务。时序图如下:


我们先来看看 shutdownNow 调用的 drainQueue 方法:

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

private List<Runnable> drainQueue() {
    BlockingQueue<Runnable> q = workQueue;
    ArrayList<Runnable> taskList = new ArrayList<Runnable>();
    q.drainTo(taskList);
    if (!q.isEmpty()) {
        for (Runnable r : q.toArray(new Runnable[0])) {
            if (q.remove(r))
                taskList.add(r);
        }
    }
    return taskList;
}

drainQueue 方法用于移除任务队列 workQueue 中的 Runnable 任务,这些未执行的任务作为 shutdownNow 方法的返回值,通知方法调用者哪些任务未执行。

如果按照上图中的执行序列,线程池的状态已经为 STOP,任务队列也被清空,但是新提交的任务 command 却被添加到任务队列中。这导致这个新任务不会被运行、也不会执行拒绝策略、也无法通过 shutdownNow 返回的任务列表通知调用者

这严重降低了线程池的健壮性,难以想象一个已提交的任务消失在线程池中!


场景 2:线程池处于运行状态,corePoolSize 设置为 0,阻塞队列的容量大于 0。

线程池刚启动时,提交任务 command 显然无法创建核心线程执行,任务会被缓冲在任务队列中,直到任务队列容量到达上限,线程池才会创建非核心线程执行任务。这导致 大量任务将不能及时被处理,甚至可能永远得不到执行

场景示意图如下(图中任务队列容量为 4,corePoolSize 等于 0):


二次检查解决了上述两种场景的问题吗?当然!!

c = ctl.get(); // -------(1)
if (isRunning(c) 
		&& workQueue.offer(command)) { // -------(2)
    int recheck = ctl.get();  // -------(3)
    if (!isRunning(recheck) && remove(command))
        reject(command);
    else if (workerCountOf(recheck) == 0) // -----(4)
        addWorker(null, false);
}

针对场景 1

如果在语句 (1) 和 语句(2) 之间,shutdownNow 被调用并执行完成,然后语句 (2) 将新任务 command 加入任务队列。在语句 (3) 重新获取最新的 ctl,此时就能得知线程池的状态已经为 STOP,使用 remove 方法回滚入队的任务,并执行 reject 方法拒绝执行任务


针对场景 2
如果线程池状态为 RUNNING,但因为线程池中线程数等于 0,语句(4) 判断为 true。因此 execute 执行 addWorker 方法创建一个非核心线程,处理排队中的任务,防止出现异步任务长时间处于队列中得不到执行的情况。


创建工作线程 addWorker()

addWorker 用于创建工作线程,我将其分为两部分分析:

  • 第一部分:根据外层死循环判断 ThreadPoolExecutor 的运行状态 是否能够创建线程。如果可以创建线程,通过内层死循环 CAS 更新状态参数 ctl,直到更新成功或线程池状态发生改变。

第一部分的含详细注释的代码如下:

private boolean addWorker(Runnable firstTask, boolean core) {
    // retry为外层循环
    retry:
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // 仅 (1) RUNNING状态 或 (2) SHUTDOWN状态+队列中仍有任务+firstTask为空 时 创建工作线程
        // firstTask为空, 说明活跃线程数不满足线程池运行的最小数量
        if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                        firstTask == null &&
                        ! workQueue.isEmpty()))
            return false;
        // for内层循环
        for (;;) {
            int wc = workerCountOf(c);
            // 如果线程数达到容量上限, 不可创建新线程
            // 如果core为true, 线程数大于等于corePoolSize, 不能创建核心线程
            // 如果 core 为 false, 线程数大于等于 maximumPoolSize, 不可以创建非核心线程
            if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            // CAS更新 ctl, 如果成功, 则退出 retry 循环, 执行创建流程
            if (compareAndIncrementWorkerCount(c))
                break retry;
            // CAS更新失败, 重新读取 ctl
            c = ctl.get();
            if (runStateOf(c) != rs)
                // 状态发生改变, 重新执行大循环
                continue retry;
            // else: 线程数改变导致CAS失败, 继续for循环即可
        }
    }
    // ... 省略第二部分
} 
  • 第二部分:状态更新成功后,执行真正的线程创建逻辑,包括:工作线程添加至 Worker 集合、启动 Thread 对象。

第二部分详细代码注释如下:

private boolean addWorker(Runnable firstTask, boolean core) {
	// ... 省略第一部分
    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
        w = new Worker(firstTask);
        final Thread t = w.thread;
        if (t != null) {
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                // 持有锁的情况下获取 ctl, 防止 shutdown、shutdownNow 导致的状态变更
                int rs = runStateOf(ctl.get());

                // 运行状态为 RUNNING或运行状态为 SHUTDOWN 且 firstTask为空 才允许启动工作线程
                if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                    // 线程可能已经启动, 抛出异常(例如: 自定义的ThreadFactory#newThread 方法多次调用返回同一个 Thread 对象)
                    if (t.isAlive())
                        throw new IllegalThreadStateException();
                    workers.add(w); // 添加到 HashSet 中
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
            if (workerAdded) {
                // worker 成功添加到 workers 集合, 在这里真正启动工作线程
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted)
            // 启动线程失败(可能线程已经启动 或 线程池状态发生改变), 将worker从workers中移除, 扣减 workerCount
            addWorkerFailed(w);
    }
    return workerStarted;
}

大部分代码阅读注释即可了解原理,这里提一下我阅读时产生疑惑的地方:

疑惑一firstTask 等于 null 代表什么?为什么判断能否创建线程时,处于 SHUTDOWN 状态还需要 firstTask 等于 null ?

 if (rs >= SHUTDOWN &&
         ! (rs == SHUTDOWN &&
                 firstTask == null &&
                 ! workQueue.isEmpty()))
     return false;

疑惑二:为什么需要在持有 mainLock 后,需要重新检查运行状态 rs


先来看疑惑一,firstTask 等于 null 出现的场景有:

  • 预启动核心线程(所有包含 prestart 单词的方法)
public boolean prestartCoreThread() {
    return workerCountOf(ctl.get()) < corePoolSize &&
        addWorker(null, true);
}

public int prestartAllCoreThreads() {
    int n = 0;
    while (addWorker(null, true))
        ++n;
    return n;
}
  • 在工作线程退出时,替换死亡的工作线程。(processWorkerExit 方法)
private void processWorkerExit(Worker w, boolean completedAbruptly) {
    // ...
    int c = ctl.get();
    if (runStateLessThan(c, STOP)) {
        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);
    }
}
  • 提交的新任务被缓冲在队列,但活跃线程数 workCount 等于 0。(execute方法)

在 addWorker 方法中,Running 状态可以创建工作线程,SHUTDOWN 状态仅可以在 firstTask 等于 null 的条件下创建线程。这符合 SHUTDOWN 状态的设计初衷:不接受新的任务、仅处理已添加至阻塞队列中的任务

除了预启动场景,execute 场景和 processWorkerExit场景 均是为了确保已经添加到任务队列中的任务不被放弃,能够成功执行。


再来看疑惑二:为什么在持有 mainLock 的情况下获取运行状态 rs?

这是为了防止 shutdown、shutdownNow 方法关闭线程池,改变运行状态。
为了确保 shutdown 和 shutdownNow 方法执行时 worker 集合的稳定,从而保证方法执行过程的原子性,这两种方法都会 在持有 mainLock 的情况下,修改 runState

因此,如果创建 worker 时 rs 发生了改变从而不应该增加工作线程,应该退出创建流程。(例如 RUNNING 变为 STOP 状态,此时不应该创建线程,因为任务都被丢弃了)。

mainLock.lock();
	int rs = runStateOf(ctl.get());
	
	// 确保运行状态 rs 可以创建新的线程
	if (rs < SHUTDOWN ||
	        (rs == SHUTDOWN && firstTask == null)) {
		// ...
	}
mainLock.unlock();

下面是我绘制的 addWorker 工作流程图,作为本小节的总结:


工作线程 Worker

ThreadPoolExecutor 中的线程资源被包装为 Worker 对象,它持有一个 Thread 对象,实现了 Runnable 接口,又继承了 AQS,因此也具有锁的性质。

需要指出的是,它没有利用 AQS 中的 CLH 队列管理等待资源的线程,因为 Worker 并 不存在多个线程争抢所有权,它的 lock 方法仅由内部持有的 线程调用。

private final class Worker
     extends AbstractQueuedSynchronizer
     implements Runnable
 {
		final Thread thread;
		/** Initial task to run.  Possibly null. */
		Runnable firstTask;
		/** Per-thread task counter */
		volatile long completedTasks;
		
		 Worker(Runnable firstTask) {
		     setState(-1); // AQS state 属性初始化为 -1
		     this.firstTask = firstTask;
		     this.thread = getThreadFactory().newThread(this);
		 }
		 // 线程的执行逻辑就是 runWorker方法
        public void run() {
            runWorker(this);
        }
        // runWorker方法中, 线程在执行任务前持有锁, 将state更改为 1
        public void lock()        { acquire(1); }
        // shutdown 关闭空闲线程时, 使用 tryLock 尝试获取锁 
        public boolean tryLock()  { return tryAcquire(1); }
        // 任务执行完成释放锁, state更改为 0
        public void unlock()      { release(1); }
        public boolean isLocked() { return isHeldExclusively(); }
		// ...
}

AQS 在 Worker 中的主要作用是维护 state 属性。Worker 构造函数中,state 初始化为 -1,执行 runWorker() 方法时会被设置为 0。state 等于 0 说明线程是空闲的,state 等于 1 说明线程正在处理任务

  • Worker#lock() 方法在仅在 runWorker 方法中被调用,线程在执行任务前调用该方法持有锁,将state更改为 1。
  • Worker#unlock() 方法在执行完任务后被调用,释放锁,将 state 更改为 0。
  • Worker#tryLock() 方法在 shutdown() 方法中被调用,用于中断空闲的工作线程,因为空闲的 Worker state 等于 0,因为 tryLock 能返回 true。

主逻辑 runWorker

runWorker 代码及详细注释如下:

    final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        // 工作线程的第一个任务, 创建核心线程 或 线程池已满创建非核心线程时, firstTask非空
        Runnable task = w.firstTask;
        w.firstTask = null;
        // 将 state 由初始值 -1 修改为 0
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
        	// getTask如果为空, 说明任务队列中已经没有任务可以执行, 工作线程正常退出
            while (task != null || (task = getTask()) != null) {
                w.lock(); 
                // 在执行任务前, 清除线程的中断标记(较为费解, 随后详细解释)
                if ((runStateAtLeast(ctl.get(), STOP) ||
                        (Thread.interrupted() &&
                                runStateAtLeast(ctl.get(), STOP))) &&
                        !wt.isInterrupted())
                    wt.interrupt();
                try {
                    // 执行任务前的钩子方法, 继承ThreadPoolExecutor的类可重写
                    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 {
                        // 执行任务完成后的钩子方法, 继承ThreadPoolExecutor的类可重写
                        afterExecute(task, thrown);
                    }
                } finally {
                    task = null;
                    w.completedTasks++;
                    w.unlock(); // state修改为0, 工作线程空闲
                }
            }
            completedAbruptly = false;
        } finally {
            // 处理线程退出:
            // 1. 从 worker 集合中移除当前工作线程
            // 2. 如果活跃线程数不满足线程池运行的最低要求, 或者线程因为执行异常而终止, 创建新线程替换
            processWorkerExit(w, completedAbruptly);
        }
    }

工作线程的运行流程概括起来为:

  1. getTask 从线程池中获取 Runnable 任务;
  2. 按照 beforeExecuteRunnable#runafterExecute 的顺序执行,beforeExecute 和 afterExecute 为 ThreadPoolExecutor 提供的两个扩展点,子类可以重写这两个方法满足打点、日志等自定义需求。
  3. 如果任务顺利执行,进行下一轮循环,通过 getTask 获取新任务。
    如果 getTask 返回 null,说明任务队列中没有任务 或者 当前线程因为线程池关闭而被中断
  4. 如果任务 或 钩子函数执行时抛出了异常,线程同样会退出,completedAbruptly 为 true。

在讲解完工作线程的主要流程后,我们来讨论下面这个 if 语句的含义:

Thread wt = Thread.currentThread();
w.lock();
if ((runStateAtLeast(ctl.get(), STOP) ||
        (Thread.interrupted() &&
                runStateAtLeast(ctl.get(), STOP))) &&
        !wt.isInterrupted())
    wt.interrupt();
// ...

这段代码执行的目的是:

工作线程 worker 已经领取了一个任务准备执行,如果线程池状态为 RUNNING 或 SHUTDOWN,应该确保当前线程的中断标记被清除,从而不影响任务的执行。Thread.interrupted() 方法会 返回当前线程的中断标记,并将线程中断标记清空

如果线程池的状态为 STOP,且当前线程未被中断,wt.interrupt() 为当前线程打上中断标记。

下面我来分类讨论,帮助大家更好的理解:

  • runStateAtLeast(ctl.get(), STOP) == true && !wt.isInterrupted() == true
    当前线程池的状态至少为 STOP,当前线程却没有中断标记。if 判断为 true,中断当前线程;
  • (runStateAtLeast(ctl.get(), STOP) == falseThread.interrupted() == true && runStateAtLeast(ctl.get(), STOP) == false
    if 判断为 false,当前线程的状态为 RUNNING 或 SHUTDOWN,且已经有一个即将执行的任务,Thread.interrupted() 将中断标记清除。
  • (runStateAtLeast(ctl.get(), STOP) == falseThread.interrupted() == true && runStateAtLeast(ctl.get(), STOP) == true!wt.isInterrupted() == true
    这种情况非常反直觉,但是有可能出现的。下图操作序列很好说明了这种情况:因为 错误地将 STOP 中断标记给清除,所以 if 也会判断为 true,执行 wt.interrupt() 中断当前线程。
  • (runStateAtLeast(ctl.get(), STOP) == falseThread.interrupted() == false && runStateAtLeast(ctl.get(), STOP) == true
    这种情况类似上一种,只是线程池状态设置为 STOP,还未中断当前线程,if 操作会返回 false。


获取任务 getTask()

工作线程通过 getTask 从任务队列中获取任务,如果 getTask 返回 null,线程就会退出 runWorker 中的死循环。

getTask 何时返回 null

条件一:线程池状态为STOP、TIDYING、TERMINATED;或者是 SHUTDOWN且工作队列为空

条件二:工作线程 wc 大于最大线程数或当前工作线程已经超时, 且还有其他工作线程或任务队列为空。

当前线程超时的条件:(【核心线程可以超时】或【线程数大于核心线程数】)且 上一轮循环从阻塞队列的 poll 方法超时返回。

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

    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c); // rs保留c的高3位, 低29位全部清零

        // 大小顺序为TERMINATED > TIDYING > STOP > SHUTDOWN > RUNNING
        // 条件一
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount(); // cas扣减线程数
            return null;
        }

        int wc = workerCountOf(c);

        // timed表示当前线程是否能够超时(设置了【核心线程超时】或线程数超过了核心线程)
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
        
        // 条件二
        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            // 可能有多个线程同时满足条件二, 需要使用cas扣减
            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; // poll取任务超时, timedOut设置为true
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

getTask 的流程图如下:


随后,我将在【工作线程的退出】章节,详细介绍 不同场景线程池回收工作线程的过程 ,会结合 getTask 方法分析。

工作线程的退出

RUNNING 状态所有任务执行完成

这种场景下,会将工作线程的数量减少到核心线程数大小。

int wc = workerCountOf(c);
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

if ((wc > maximumPoolSize || (timed && timedOut))
        && (wc > 1 || workQueue.isEmpty())) {
    if (compareAndDecrementWorkerCount(c))
        return null;
    continue;
}

timed 表示是否允许线程因为超时被回收;timedOut 记录上一轮循环中,线程从阻塞队列获取任务是否超时了。

假设线程池核心线程数为2,最大线程数为4。线程数低于核心线程数时,使用execute 提交任务便会创建核心线程;线程数达到 2 后,任务被添加至阻塞队列,如果阻塞队列也满了,将工作线程逐渐增加到 4。当全部任务执行完成后:

  1. 工作队列为空,四个线程阻塞在 workQueue.poll 上,各自等待 keepAliveTime 时间后,超时返回,timedOut 设置为 true。

  2. 进入下一轮循环,因为 wc 等于 4 大于 corePoolSize=2,因此四个线程 timed 均为 true,从而 timed&timedOut 为 true 且 当前任务队列为空,情况二成立,4 个线程都可以被超时回收。

  3. 四个线程尝试 CAS 扣减 wc 为 3(仅有一个线程能扣减成功,getTask 返回 null)。其余三个线程继续循环,直到线程数达到核心线程数,timed 等于 false。

shutdown 关闭线程池

调用 shutdown() 后,线程池状态流转为 SHUTDOWN,随后向所有的空闲工作线程发送中断信号

public void shutdown() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        advanceRunState(SHUTDOWN);
        interruptIdleWorkers(); // 中断所有空闲线程
        onShutdown(); // hook for ScheduledThreadPoolExecutor
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
}

处于 getTask 获取任务阶段的工作线程是空闲的,并没有锁定 Worker。我将分三种情况探讨工作线程如何响应中断信号。

  • 任务全部完成,所有线程在等待;
  • 任务队列中积压了大量任务,所有线程在繁忙;
  • 队列中剩余的任务少于空闲线程数;
所有线程等待新任务
// getTask(): 条件一
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
    decrementWorkerCount(); // cas扣减线程数
    return null;
}
...
try {
    // 超出核心线程数时, poll等待存在超时时间; 反之, 使用take阻塞
    Runnable r = timed ?
        workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
        workQueue.take();
    if (r != null)
        return r;
    timedOut = true; // poll取任务超时, timedOut设置为true
} catch (InterruptedException retry) {
    timedOut = false;
}

中断信号将阻塞的线程唤醒,进入下一轮循环。当到达条件一处,检查到 rs 等于SHUTDOWN,且工作队列为空,满足条件,扣减线程数后返回null。在runWorker 中退出循环,结束线程。

所有线程繁忙

此时任务队列中积压了很多任务,工作线程因为 shutdown 而被中断,在获取任务时 调用 poll 或 take 方法都会抛出 InterruptedException 异常,然后被 catch 捕获,重新进行循环。

第二次循环到达条件一,虽然 rs 为 SHUTDOWN,但是工作队列非空,不满足退出条件。

// 工作队列非空, 条件1不满足
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
    decrementWorkerCount();
    return null;
}

timedOut 为 false,不是因为 poll 超时而返回,因此条件 2 也不满足:

// timedOut false
if ((wc > maximumPoolSize || (timed && timedOut))
        && (wc > 1 || workQueue.isEmpty())) {
    if (compareAndDecrementWorkerCount(c))
        return null;
    continue;
}

因此,shutdown 方法在线程池繁忙的情况下,相当于让 正在获取任务的线程空转了一次,不影响线程池运行。

队列中剩余少量的任务

假设情形

线程池状态已经是SHUTDOWN,但任务队列中剩余两个任务,A、B、C、D四个线程同时通过条件一和条件二,尝试从阻塞队列中获取任务。线程A、B成功获取任务,而线程 C、D因队列为空而阻塞。

线程A、B执行完任务后再次调用 getTask(),条件一的判断为true(线程池运行状态为SHUTDOWN且工作队列为空),于是返回 null,线程退出 runWorker 死循环,准备进行回收。

final void runWorker(Worker w) {
    boolean completedAbruptly = true;
    try {
        while (task != null || (task = getTask()) != null) {
            ...
        }
    }finally {
    // 回收退出的线程
    processWorkerExit(w, completedAbruptly);
}

在回收前,还需要执行 processWorkerExit 方法。在该方法中会将 worker 移除出 worker 集合,并调用tryTerminate()。

private void processWorkerExit(Worker w, boolean completedAbruptly) {
    // 执行任务时抛出异常退出, 而非getTask()返回null退出, 需要更新ctl属性反映线程数的变化
    if (completedAbruptly) 
        decrementWorkerCount();

    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        completedTaskCount += w.completedTasks; // 统计完成的任务数
        workers.remove(w); // 将Worker对象移除工作线程集合
    } finally {
        mainLock.unlock();
    }

    tryTerminate();
    ...
}

在 tryTerminate 中,线程A、B判断线程池状态为 SHUTDOWN 且工作队列为空,不会在第一个 if 处返回。

然后判断出当前workers中的工作线程数不为0(因为线程C、D正阻塞),然后调用 interruptIdleWorkers(ONLY_ONE)

注意:此时线程A、线程B的线程数已经从ctl扣减,Worker实例也从workers中移除。

final void tryTerminate() {
    for (;;) {
        int c = ctl.get();
        if (isRunning(c) ||
            runStateAtLeast(c, TIDYING) ||
            (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
            return;
        // 线程池状态为SHUTDOWN, 但仍然有线程阻塞在take或poll方法处
        if (workerCountOf(c) != 0) { // Eligible to terminate
            interruptIdleWorkers(ONLY_ONE);
            return;
        }
        ...
    }
}

interruptIdleWorkers 的入参 onlyOne 为true,因此只会中断一个空闲线程,然后break循环。假设先中断线程C,线程C从阻塞中被唤醒,抛出InterruptedException异常,被 catch 住异常后重新进行一轮循环,发现条件一满足,更新 ctl 并返回null。

private void interruptIdleWorkers(boolean onlyOne) {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        for (Worker w : workers) {
            Thread t = w.thread;
            // 正在执行任务的Worker是无法获取锁的, 因此这里只能回收空闲线程
            if (!t.isInterrupted() && w.tryLock()) {
                try {
                    t.interrupt();    
                } catch (SecurityException ignore) {
                } finally {
                    w.unlock();
                }
            }
            // 仅中断一个空闲线程
            if (onlyOne)
                break;
        }
    } finally {
        mainLock.unlock();
    }
}

随后,线程 D 可以由上一个退出的线程中断唤醒(例如线程 C),从而让工作线程优雅地退出。

写在最后

感谢各位读者阅读本片博客,本篇博客的创作过程中参考了大量资料,笔者也详细阅读了 ThreadPoolExecutor 源码。笔者将很多阅读源码的思考融入本篇博客,尽可能去体会 Doug Lea 大神每一行代码的用意。这些细节可能很少有博客涉及,因此很可能存在纰漏和理解错误。如果有异见,欢迎在评论区指教,笔者将虚心倾听

创作过程耗时费力,但我乐在其中(钻研源码的过程和分享知识是让人快乐的事情),如果大家喜欢这种图文结合、代码详细注释的写作风格,就给我点一个免费的赞吧!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值