【Java 多线程】5 - 9 深入了解线程池

§5-9 深入了解线程池

下文内容参考自:

Java并发系列终结篇:彻底搞懂Java线程池的工作原理 - 掘金 (juejin.cn)

5-9.1 回顾线程池的工作流程

手动创建线程来执行任务,这种操作虽然能够满足多线程的实现,但是这会增加创建和销毁线程的开销。为了减少这一开销,简化操作,引入了线程池。通常使用并发包中的工具类 Executors 创建并返回一个线程池。可根据实际开发的需要,创建不同类型的线程池。通常而言,会直接使用 ThreadPoolExecutor 的构造器自定义线程池。

ThreadPoolExecutor 的构造方法具有许多重载,在底层都会调用具有七个参数的重载构造器:

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)
        // 即存在关系 maximumPoolSize > corePoolSize > 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),则无论当前池中是否有空闲核心线程,线程池都会新建核心线程,用新线程执行任务;

  2. 当核心线程已满且均已满载运行时,再次提交任务则会将任务放入工作队列(阻塞队列)中排队等待。当有空闲核心线程时,核心线程会从工作队列(workQueue)中抽取第一个任务开始执行;若队列为空,则阻塞线程;

  3. 当核心线程已满载运行,并且工作队列已满时,再次提交任务,若总任务数小于最大线程数(maximumPoolSize),线程池则会新建临时线程执行新提交的任务。若提交的总任务数大于最大任务数,则新提交的任务会触发任务拒绝策略(handler);

    任务拒绝策略有以下四种:

    • 放弃任务策略(AbortPolicy):默认策略,拒绝执行新提交的任务并抛出异常 RejectedExecutionException
    • 丢弃任务策略(DiscardPolicy):拒绝执行并直接丢弃新提交的任务;
    • 丢弃最老任务策略(DiscardOldestPolicy):丢弃工作队列中等待时间最长的任务(不推荐);
    • 调用者执行策略(CallerRunsPolicy):线程池拒绝执行新提交的任务,让调用者线程执行所提交的任务;
  4. 当临时线程空闲时间超过一定时间(keepAliveTime)后,临时线程将会被销毁,使得线程池中的线程数目总是保持在核心线程数;

  5. 若允许核心线程超时销毁(allowCoreThreadTimeOut(boolean) 设为 true),则线程池中的所有空闲线程将会在超时时间后销毁;

5-9.2 ThreadPoolExecutor 的数据结构

在开始从源码角度分析线程池的行为前,得先要了解 ThreadPoolExecutor 的数据结构。

public class ThreadPoolExecutor extends AbstractExecutorService {
    // 线程数比特数,Integer.SIZE = 32,该值实际为 29
    private static final int COUNT_BITS = Integer.SIZE - 3;
    // 线程数掩码,即 29 个连续的 1(0001 1111 1111 1111 1111 1111 1111 1111)
    private static final int COUNT_MASK = (1 << COUNT_BITS) - 1;

    // 线程池的运行状态存储在高位中
    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;

    // 打包与解包 ctl
    private static int runStateOf(int c)     { return c & ~COUNT_MASK; }
    private static int workerCountOf(int c)  { return c & COUNT_MASK; }
    private static int ctlOf(int rs, int wc) { return rs | wc; }
    
    // 工作队列
    private final BlockingQueue<Runnable> workQueue;
    
    // 线程池中线程(线程队列),由 HashSet 存储,这是一个线程不安全的集合
    private HashSet<Worker> workers = new HashSet<>();
    
    // 默认拒绝策略
    private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();
    
    // 可重入锁,在添加工作线程时使用
    private final ReentrantLock mainLock = new ReentrantLock();
    
    // 封装了工作线程的内部类 Worker
    private static class Worker 
        extends AbstractQueuedSynchronizer 
        implements Runnable {...}
}

可以看到,ThreadPoolExecutor 内部涉及大量的位运算内容,主要用于线程池运行状态中。下面先简要解释这些运行状态:

  • RUNNING:运行中。线程池创建完成后就处于这个状态中,接受新任务的提交与执行;
  • SHUTDOWN:关机。线程池调用 shutdown 后处于这个状态,不接受新任务提交,但会继续执行已提交的任务;
  • STOP:停机。线程池调用 shutdownNow 后处于这个状态,不接受新任务提交,也不会执行已提交的任务,并中断正在执行任务的线程;
  • TIDYING:整理。当线程池处于 SHUTDOWN 状态且所有任务都执行完毕后,线程池会进入这个状态;或当线程池处于 STOP 状态且线程池中没有正在执行的任务时,线程池会进入这个状态。

现在来看看这五个状态常量是如何计算的。

// 注意整型在计算机中以补码的形式存储

// -1 的补码为 1111 1111 1111 1111 1111 1111 1111 1111,左移 29 位
// 二进制表示为 1110 0000 0000 0000 0000 0000 0000 0000(负值)
private static final int RUNNING    = -1 << COUNT_BITS;		// -5 3687 0912
// 二进制表示为 0000 0000 0000 0000 0000 0000 0000 0000
private static final int SHUTDOWN   =  0 << COUNT_BITS;		// 0
// 二进制表示为 0010 0000 0000 0000 0000 0000 0000 0000
private static final int STOP       =  1 << COUNT_BITS;		//  5 3687 0912
// 二进制表示为 0100 0000 0000 0000 0000 0000 0000 0000
private static final int TIDYING    =  2 << COUNT_BITS;		// 10 7374 1824
// 二进制表示为 0110 0000 0000 0000 0000 0000 0000 0000
private static final int TERMINATED =  3 << COUNT_BITS;		// 16 1061 2736

可以看到,这五个状态常量具有大小关系,这些大小关系在判断线程池运行状态中具有重要作用。

另一个值得注意的是一个原子整型 ctl,这个变量用于记录线程池状态和线程数量。

// 使用 AtomicInteger 保证线程安全,操作具有原子性
private static final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

// rs = runState(运行状态),wc = workerCount(工作线程数)
private static int ctlOf(int rs, int wc) { return rs | wc; }

实际上就是 1110 0000 ... 和 0 做按位或运算,完成初始化。这里,ctl 变量将整型变量分割为高 3 位和低 29 位。其中,高位用于表示线程池状态,低位表示线程数目。

再来看看和该变量有关的打包 / 解包算法,也运用了位运算。

// 打包与解包 ctl
private static int runStateOf(int c)     { return c & ~COUNT_MASK; }	// 获取高位,得到线程池运行状态
private static int workerCountOf(int c)  { return c & COUNT_MASK; }		// 获取低位,得到线程数目
private static int ctlOf(int rs, int wc) { return rs | wc; }			// 打包 ctl

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

private static boolean runStateAtLeast(int c, int s) { return c >= s; }	// 大于等于(至少)

// 判断线程池是否处于运行状态。RUNNING 是唯一一个为负值的状态常量
private static boolean isRunning(int c) { return c < SHUTDOWN; }

5-9.3 向线程池提交并执行任务

若通过 Executors 工具类创建线程池,那么返回的是接口 ExecutorService 以多态的形式指向的 ThreadPoolExecutor。这种情况下,我们只能够调用接口中的方法来提交任务并执行。一般地,我们调用 submit(Runnable), submit(Runnable, T)submit(Callable) 的方法提交任务。以 submit(Runnable) 为例:

// 该方法由抽象实现类 AbstractExecutorService 实现,ThreadPoolExeutor 继承了 AES
public Future<?> submit(Runnable task) {
    if (task == null) throw new NullPointerException();		// 任务参数合法性检查,要求非空
    RunnableFuture<Void> ftask = newTaskFor(task, null);	// 使用 FutureTask 包装任务,返回空结果
    execute(ftask);											// 调用 execute 方法实际执行任务
    return ftask;											// 将 FutureTask 返回,可用于控制任务
}

若为自定义线程池,则不需要通过 submit 方法提交任务,这个操作交由方法 execute 执行。因此,execute 是线程池中的核心方法,用于执行所提交的任务。

5-9.3.1 execute 提交并执行任务

public void execute(Runnable command) {
    // 要求提交的任务非空
    if (command == null)
        throw new NullPointerException();
    
    // 获取 ctl
    int c = ctl.get();
    
    // 1. 检查线程池中线程数是否不足核心线程数
    if (workerCountOf(c) < corePoolSize) {
        // 线程数不足核心线程数,添加核心线程
        if (addWorker(command, true))
            return;
        // 添加失败,ctl 的值可能发生改变,重新读取 ctl 的值
        c = ctl.get();
    }
    
    // 2. 在线程池正在运行时尝试向工作队列中添加新任务
    // 调用阻塞队列的 offer 方法,该方法是非阻塞的
    if (isRunning(c) && workQueue.offer(command)) {
        // 重新检查 ctl 的值,因为 ctl 可能发生改变
        int recheck = ctl.get();
        
        // 线程池不在运行状态(关机),尝试从队列中移除新提交的任务
        if (! isRunning(recheck) && remove(command))
            // 移除成功后,触发任务拒绝策略
            reject(command);
        // 3. 否则,线程池正在运行,若工作线程数为零,则添加线程,但不执行任务
        else if (workerCountOf(recheck) == 0)
            // 该方法的第二个参数用于确定线程是否属于核心线程
            addWorker(null, false);
    }
    // 4. 工作队列容量已满,拒绝接受任务,触发任务拒绝策略
    else if (!addWorker(command, false))
        reject(command);
}

简单总结一下,execute 方法的运行逻辑如下:

  1. 若线程池中的线程数少于核心线程数,则直接添加核心线程,让核心线程执行任务;
  2. 核心线程数已满,则在线程池仍在运行的情况下尝试往工作队列中添加任务;否则(线程池已关闭或工作队列已满)触发任务拒绝策略;
  3. 成功地往工作队列添加任务后,ctl 的值可能发生改变,因此重新检查该值,即再次检查线程池的运行状态和线程数。若线程池已关闭,则从队列中移除任务,触发拒绝任务策略;否则,若尚未添加工作线程,则创建一条不执行指定任务的非核心线程。由于任务已经添加到工作队列中,创建线程后从工作队列中取出任务执行。

这样来看,该方法的执行流程和前文线程池的工作流程一致。

5-9.3.2 addWorker 添加工作线程

下面就来看看实际添加工作线程的方法 addWorker 的执行流程。我们将其拆分成两部分来看,先看上半部分。

private boolean addWorker(Runnable firstTask, boolean core) {
    // 一个标签,作用类似于 goto,实现循环跳转
    retry:
    // CAS 自旋
    // 获取 ctl 的值
    for (int c = ctl.get();;) {
        // 仅在必要时检查队列是否为空
        if (runStateAtLeast(c, SHUTDOWN)
            && (runStateAtLeast(c, STOP)
                || firstTask != null
                || workQueue.isEmpty()))
            // 条件不满足时(主要为线程池运行状态),拒绝添加,添加失败
            return false;

        // 在线程池处于运行状态下,或在关机状态中(无新任务提交),需要处理队列中的任务时
        // 判断线程池数目限制能否允许新建线程执行未完成的任务
        for (;;) {
            // 读取线程数,与线程池实际线程数目限制比较
            if (workerCountOf(c)
                // 根据 core 确定添加线程的类型,从而确定线程数目限制(核心线程数和最大线程数)
                // 将线程数目限制与线程数掩码做与运算,得到线程池实际的线程数目限制(由掩码决定)
                >= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK))
                // 满限或超限,则拒绝添加,添加失败
                return false;
            
            // 否则,线程数未达到上限,可继续添加
            // 调用 CAS 算法,增加工作线程数(修改 ctl 的值)
            if (compareAndIncrementWorkerCount(c))
                break retry;	// 直接退出整个嵌套循环
            c = ctl.get();  	// 重新读取 ctl
            
            // 未能成功修改工作线程数
            // 可能 ctl 发生变化,线程池状态发生改变,测试线程池是否变化为(至少)关机状态
            if (runStateAtLeast(c, SHUTDOWN))
                // 若线程池已关闭,则退出内层循环
                // 重新执行外层循环,再次检查线程池状态、任务、队列条件是否满足
                continue retry;
            // 否则,由于工作线程数发生变化,CAS 失败,重试内层循环,再次尝试 CAS 增加线程数
        }
    }

    // ...
    return workerStarted;
}

可以看到,这一部分主要是进行添加线程前的检查操作。考虑到线程池在多线程的环境下操作,这里采用了 CAS + 自旋的方法处理,嵌套形成了两层循环。内层循环的操作简单明了,这里需要留意的是外层循环首先运行的 if 语句块。

这里的 if 语句块的条件语句设计得很巧妙,充分利用了逻辑运算短路的特点,配合线程池状态常量的大小关系,尽可能地覆盖了多种不同情况下导致的不允许添加线程的情况。这条表达式最终是一个与表达式,要想让该表达式为真,其第一个操作数必须为真,即线程池状态至少为关机状态。那么,现在只需要看第二个操作数为真的情况。

第二个操作数是一个三操作数的内嵌或表达式,三个操作数中至少有一个为真就可使得整个条件语句为真。这样,表达式为真的情况主要有以下三种:

  • 当线程池处于关机状态,且在无具体任务的情况下(新任务为空),工作队列已清空时,拒绝添加任务。这时方法直接返回,由上游方法 execute 处理,触发任务拒绝策略;
  • 当线程池处于关机状态,且在具有具体任务的情况下(新任务不为空),则直接拒绝任务,方法直接返回,由上游方法 execute 处理,触发任务拒绝策略;
  • 当线程池处于停机状态,方法直接返回,由上游方法 execute 处理,触发任务拒绝策略。

可以看到,executeaddWorker 配合实现了线程池的工作流程,符合前文的描述。外层循环先判断线程池的运行状态,若运行状态需要继续处理任务,则进入内层循环判断线程数目。在关机状态下,对于工作队列中尚未完成的任务,以及在运行状态下,对于新提交的任务,由 addWorker 的后半部分完成。

Worker 的后半部分涉及到对 Worker 类的操作。为了更易于理解,先来看看 Worker 的数据结构。

private final class Worker 
    extends AbstractQueuedSynchronizer 
    implements Runnable {
    // AQS 支持序列化,因此该类也支持序列化。但实际上该类并不会被序列化,这里提供给序列化版本号以抑制 javac 的警告
    private static final long serialVersionUID = 6138294804551838833L;
    
    // 该 Worker 正在运行的线程,由线程工厂提供(失败则为 null)
    final Thread thread;
    
    // 执行的初始任务,可能为 null
    Runnable firstTask;
    
    // 每线程任务计数器
    volatile long completedTasks;
    
    // 构造器
    // 从线程工厂中创建线程,提供初始任务(可能为空)
    Worker(Runnable firstTask) {
        // AQS 的方法,设置锁的状态
        setState(-1); 					// 防止中断,直至调用 runWorker
        this.firstTask = firstTask;		// Worker 封装的任务内容
        this.thread = getThreadFactory().newThread(this);
    }
    
    // 重写自 Runnable 的方法
    public void run() { runWorker(this); }
    
    // 与锁有关的方法
    // 0 代表解锁状态,1 代表上锁状态(非可重入锁)
    protected boolean tryRelease(int unused) {
        setExclusiveOwnerThread(null);
        setState(0);
        return true;
    }
    public void lock()        { acquire(1); }
    public boolean tryLock()  { return tryAcquire(1); }
    public void unlock()      { release(1); }
    public boolean isLocked() { return isHeldExclusively(); }
    
    // ...
}

注意到,由于 Worker 继承了 AQS,它实现了锁的功能。该锁是一个非可重入的独占锁。利用了不可重入的特性,表示线程的执行状态。此外,Worker 实现了 Runnable 接口,允许调用 run 方法开启新的线程执行任务,但该方法内部调用了另一个方法,者在稍后会提到。

现在看看 addWorker 的后半部分。

private boolean addWorker(Runnable firstTask, boolean core) {
    // ...
    // 条件检查部分(见前文)
    
    boolean workerStarted = false;		// 工作线程是否已启动
    boolean workerAdded = false;		// 工作线程是否已添加
    Worker w = null;					// 声明工作者类的实例
    
    try {
        // 实例化 Worker,内部封装了工作线程
        w = new Worker(firstTask);
        // 获取 Worker 对象封装的工作线程
        final Thread t = w.thread;
        
        if (t != null) {
            // 获取锁引用,并调用锁,保证线程安全(workers 集合线程不安全)
            // 使用锁而不使用并发集合,一方面性能表现更好、更灵活
            // 一方面也防止多线程同时尝试销毁同一空闲线程(中断空闲线程),保证中断的序列化,防止不必要的中断风暴,影响性能
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            
            try {
                // 持有锁时重新检查 ctl 的值
                // 线程工厂创建线程失败时(线程为null)退出
                // 或者在取到锁之前线程池就已关闭,也退出
                int c = ctl.get();

                // 再次利用逻辑运算的短路特性,根据线程池的不同状态作出反应
                // 仅在线程池正在运行时,或线程池处于关机状态(拒绝新任务)时处理已提交的任务
                if (isRunning(c) ||
                    (runStateLessThan(c, STOP) && firstTask == null)) {
                    // 保证线程处于新建状态(new)
                    if (t.getState() != Thread.State.NEW)
                        throw new IllegalThreadStateException();
                    
                    // 将线程添加到线程池的线程队列中
                    workers.add(w);
                    // 修改布尔变量
                    workerAdded = true;
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                }
            } finally {
                // 在 finally 语句块中释放锁
                mainLock.unlock();
            }
            
            // 成功添加线程后,启动该线程
            if (workerAdded) {
                // 启动线程,会调用 Runnable.run 方法开启新线程执行任务
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        // 不满足添加添加线程的条件(关机状态拒绝执行新任务、停机状态拒绝执行所有任务)
        if (! workerStarted)
            // 调用 addWorkerFailed 回滚创建工作线程的操作
            // addWorkerFailed 方法使用 mainLock 保护数据,尝试从线程队列中删除新建的工作线程
            // 同时,使用 CAS 算法减少 ctl 的值,修改线程数量(该值在上半部分利用相同算法自增)
            // 然后,内部再调用 tryTerminate 方法,尝试将线程池状态修改为 TERMINATED(终止)
            addWorkerFailed(w);
    }
    
    // 最终返回工作线程是否开启,执行任务
    return workerStarted;
}

该方法的返回值用于指示是否成功添加新的工作线程并启动执行。若线程状态允许(正在运行、关机状态但队列仍有任务)、线程数目允许,则会成功新建线程并启动之,方法返回 true

5-9.3.4 runWorker 执行线程任务

由于 Worker 包装的线程由线程工厂通过方法 newThread(this) 得到,而 thisWorker)本身又实现了 Runnable 并重写了 run 方法,那么,调用 start 启动线程,就一定会调用 run 方法。Worker.run 方法中只是调用了 runWorker(this) 方法,现在来看看该方法的实现。

final void runWorker(Worker w) {
    // 获取正在执行任务的当前工作线程(Worker.thread)
    Thread wt = Thread.currentThread();
    // 获取工作线程正在执行的任务
    Runnable task = w.firstTask;
    // 重置 Worker 对象中封装的任务
    w.firstTask = null;
    
    // Worker 本身是一个不可重入独占锁,解锁以允许中断
    w.unlock();
    boolean completedAbruptly = true;
    
    try {
        // 循环取任务执行
        // 若工作线程本身具有需要执行的任务,则直接执行;否则,从工作队列中取任务执行
        while (task != null || (task = getTask()) != null) {
            // 工作线程上锁,表示线程繁忙
            w.lock();
            // 若线程池已停机,则确保线程被中断
            // 否则,确保线程未被中断
            // 这种情况下需要二次确认运行状态来处理清除中断状态的同时,停机竞争的问题
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 (Thread.interrupted() &&
                  runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            
            try {
                // 执行前的动作,方法不做任何事情
                beforeExecute(wt, task);
                try {
                    // 执行任务
                    task.run();
                    // 执行后的动作,方法也不做任何事情
                    afterExecute(task, null);
                } catch (Throwable ex) {
                    // 执行任务时抛出异常,捕获后直接抛出,线程退出
                    afterExecute(task, ex);
                    throw ex;
                }
            } finally {
                // 线程任务执行完毕(或遇到异常而中止)
                // 清除 Worker 封装的任务,解锁
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        // 由于超时取不到任务,或由于执行任务时抛出异常,线程则应当被销毁
        processWorkerExit(w, completedAbruptly);
    }
}

可以看到,线程开启执行后,不断循环,尝试从工作队列中取出任务来执行,节省了反复创建线程的开销。若由于超时或线程数超限而取不到任务,或者任务在执行过程中抛出异常,线程都会调用 processWorkerExit 销毁线程。线程尝试取任务时,调用 getTask 方法,这个方法会自旋,同时其内部也可能会调用阻塞方法从工作队列中取任务,下面来看看 getTask 的实现。

5-9.3.5 getTaskprocessWorkerExit 取任务、替换和销毁线程

private Runnable getTask() {
    // 刚开始执行,线程尚未取任务,不存在超时情况
    boolean timedOut = false; // 取任务(poll(), take())是否超时?

    for (;;) {
        // 重复循环时,总是先获取 ctl 的值
        int c = ctl.get();

        // 仅在必要时查询工作队列是否为空
        // 这里再次利用了逻辑运算的短路特性,该判断决定线程是否需要被销毁具有任务可取
        if (runStateAtLeast(c, SHUTDOWN)
            && (runStateAtLeast(c, STOP) || workQueue.isEmpty())) {
            // 若线程池已关闭,且工作队列已空,则无任务可取,直接返回空任务
            // 方法返回后,回到 runWorker,循环终止,由 processWorkerExit 决定线程是否销毁
            // 更新(自减)线程数
            decrementWorkerCount();
            return null;
        }

        // 获取工作线程数
        int wc = workerCountOf(c);

        // 工作线程是否需要被销毁?
        // 若需要,则说明:
        // 1. 核心线程需要被销毁,在没有任何工作可执行时,线程池会销毁所有线程
        // 2. 只需要销毁临时线程,在线程数大于核心线程数时销毁临时线程
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

        // 这里再次巧妙地利用逻辑运算短路的特性,返回空任务,让 processWorkerExit 销毁线程
        // 该条件语句只在以下三种情况下为真:
        // 1. 工作线程数超限,这时短路或运算的右操作数,尝试用 CAS 修改线程数后返回空任务
        // 2. 工作线程数未超限,需要销毁线程且已超时,线程数大于 1 时,尝试用 CAS 修改线程数后返回空任务
        // 3. 工作线程数未超限,需要销毁所有线程且已超时,仅剩一条核心线程时,在工作队列为空时返回空任务
        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            // 线程进入方法执行第一轮循环时,不存在超时,只会在线程数超限时返回空任务
            if (compareAndDecrementWorkerCount(c))
                return null;
            // 若 CAS 修改线程数失败,则 ctl 可能发生改变,重新执行循环,再次尝试
            continue;
        }

        try {
            // 从工作队列中取任务,成功取得则不为 null,否则为 null
            // 取任务的过程中若被中断,抛出 InterruptedException,由 catch 捕获处理
            // 若需要销毁线程,则调用超时方法从工作队列中取出第一个任务
            // 不需要销毁线程,则调用阻塞方法从工作队列中取出第一个任务
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
            	workQueue.take();
            if (r != null)
                // 成功取出任务,返回任务,交给线程执行
                return r;
            // 无论是否需要销毁线程,长时间取不到任务,或无任务可取,则发生超时
            timedOut = true;
        } catch (InterruptedException retry) {
            // 线程在取任务的过程中(阻塞)被意外中断,重设超时布尔变量,重新循环,检查线程池状态
            timedOut = false;
        }
    }
}

取出任务的操作到此为止,方法会不断尝试取出任务,直至超时或无任务可取时返回空任务。这个方法的运行逻辑符合前文线程池工作流程有关空闲线程存活时间的描述。和销毁空闲线程有关的方法是 processWorkerExit 方法,该方法在 runWorker 中调用。实际上,该方法不只是会销毁线程,还会替换线程。现在来看看这个方法。

private void processWorkerExit(Worker w, boolean completedAbruptly) {
    // 任务是否突然“完成”,即是否由于抛出异常而意外中止
    // 若为是,即线程任务突然完成,则应当替换线程,线程数并没有调整
    if (completedAbruptly)
        decrementWorkerCount();

    // 使用可重入锁保护 workers 集合
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        completedTaskCount += w.completedTasks;
        // 将线程从线程队列中移除
        workers.remove(w);
    } finally {
        mainLock.unlock();
    }

    // 尝试终止线程池
    tryTerminate();

    // 获取 ctl 的值
    int c = ctl.get();
    // 线程池在运行中或已关机
    if (runStateLessThan(c, STOP)) {
        // 空闲线程由于长时间取不到任务,应当销毁
        if (!completedAbruptly) {
            // 线程池中的最小线程数
            // 若需要销毁核心线程,则线程池中最终不留有一条线程,最小线程数为 0
            // 若需要保留核心线程,则线程池的最小线程数为核心线程数
            int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
            
            // 若需要销毁所有线程,但队列仍有任务未完成,则应当至少保留一条线程继续工作
            if (min == 0 && ! workQueue.isEmpty())
                min = 1;
            
            // 否则,只需要维持线程池线程数目为核心线程数即可
            if (workerCountOf(c) >= min)
                // 不需要替换线程,方法直接返回,回到 runWorker
                // runWorker 执行结束,进一步返回到 run,线程执行结束,线程销毁
                return;
        }
        
        // 若方法能运行到此处,说明线程由于抛出异常而意外中止
        // 再次调用 addWorker 添加新线程以替换中止线程,方法运行结束后,逐步返回到 run 方法也返回,中止线程被销毁
        // 由于 addWorker 在允许添加线程的情况下会用 CAS 自增线程数,因此应当先 CAS 自减线程数,以确保线程数不变
        // 新的线程会从队列中抽取任务来执行
        addWorker(null, false);
    }
}

至此,向线程池提交任务并执行的全流程结束。

5-9.4 线程池的拒绝任务策略

若线程池正处于关机状态,或线程池线程容量已满,提交新任务会触发任务拒绝策略。从前文可知,会调用 reject 方法,该方法的实现为:

final void reject(Runnable command) {
    handler.rejectedExecution(command, this);
}

方法实际上通过 ThreadPoolExecutor 的成员变量 handler 调用 rejectedExecution 方法实现。该成员变量是 ThreadPoolExecutor 的内部类,所有拒绝任务策略都是该类的一个内部类,实现了 RejectedExecutionHandler 函数式接口,该接口有且只有一个方法:

// 方法会在没有解决办法的时候抛出非受查异常 RejectedExecutionException
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);

AbortPolicy 放弃任务策略

该策略的方法实现如下:

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    throw new RejectedExecutionException("Task " + r.toString() +
                                         " rejected from " +
                                         e.toString());
}

方法直接放弃新任务,并抛出异常 RejectedExecutionException

CallerRunsPolicy 调用者执行策略

该策略的方法实现如下:

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    if (!e.isShutdown()) {
        r.run();
    }
}

方法会在线程池未关闭的情况下,由当前线程(调用者)执行该任务。

DiscardPolicy 丢弃任务策略

该策略的方法实现如下:

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { }

方法什么也不做,相当于抛弃提交的任务。

DiscardOldestPolicy 丢弃最老任务策略

该策略的方法实现如下:

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    if (!e.isShutdown()) {
        e.getQueue().poll();
        e.execute(r);
    }
}

方法会在线程池未关闭的情况下,从线程池的工作队列中取出下一个任务并丢弃它,并让线程池直接开始执行新提交的任务。

5-9.5 关闭线程池

不再使用线程池时,应当关闭线程池以释放资源。一般而言,我们会使用 shutdown 方法关闭线程池。这样即使线程池进入关闭状态,不接受新任务,线程池仍会继续执行已提交的任务。

现在来看看 shutdown 的方法实现。

public void shutdown() {
    // 获取锁对象
    final ReentrantLock mainLock = this.mainLock;
    // 上锁
    mainLock.lock();
    try {
        // 调用 SecurityManager 检查是否可以关闭线程
        checkShutdownAccess();
        // 将线程池状态过渡到关机状态
        advanceRunState(SHUTDOWN);
        // 中断所有空闲线程
        interruptIdleWorkers();
        onShutdown(); // 用于计划线程池的关机钩,在此处不做任何事情
    } finally {
        // 解锁
        mainLock.unlock();
    }
    
    // 尝试终止线程池
    tryTerminate();
}

而方法中尝试中断空闲线程的方法 interruptIdleWorkers 方法实现为:

private void interruptIdleWorkers() {
    // 调用了一个带参数的中断空闲线程方法
    interruptIdleWorkers(false);
}

// 形参用于控制是否只中断一条线程
private void interruptIdleWorkers(boolean onlyOne) {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        // 遍历工作线程队列
        for (Worker w : workers) {
            Thread t = w.thread;
            // Worker 本身继承了 AQS,是不可重入独占锁
            // 一旦上锁,意味着线程处于忙碌状态,则跳过该线程
            if (!t.isInterrupted() && w.tryLock()) {
                try {
                    t.interrupt();
                } catch (SecurityException ignore) {
                } finally {
                    w.unlock();
                }
            }
            // 是否只中断一条线程
            if (onlyOne)
                break;
        }
    } finally {
        mainLock.unlock();
    }
}

现在,我们来详细看看尝试终止线程池的方法。

final void tryTerminate() {
    // CAS 自旋
    for (;;) {
        // 获取 ctl 的值
        int c = ctl.get();
        // 若线程池正在运行,或线程池已处于整理状态,或线程池仍有待处理的任务,则取消终止,方法返回
        if (isRunning(c) ||
            runStateAtLeast(c, TIDYING) ||
            (runStateLessThan(c, STOP) && ! workQueue.isEmpty()))
            return;
        // 否则,若线程池中仍有线程
        if (workerCountOf(c) != 0) { // 符合条件终止
            // 中断单条空闲线程
            interruptIdleWorkers(ONLY_ONE);
            // 由于线程池中还留有线程未被销毁,不能够终止线程池,方法返回
            return;
        }

        // 上锁
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            // 利用 CAS 尝试修改 ctl 的高位状态值为整理状态
            if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
                try {
                    // 执行器在被终止后应当调用的方法,默认实现不做任何事情
                    terminated();
                } finally {
                    // 将线程池状态修改为终止状态
                    // 这里由于上锁处理,可以不使用 CAS,直接修改,防止 CAS 失败导致线程池卡在整理状态中
                    ctl.set(ctlOf(TERMINATED, 0));
                    // 主要用于调用了 awaitTermination 方法等待线程池终止
                    // 通知所有等待终止的线程,线程池已终止,结束等待终止方法
                    // 该方法可由用户调用,等待线程并不是池中线程
                    termination.signalAll();
                }
                return;
            }
        } finally {
            mainLock.unlock();
        }
        // CAS 失败,ctl 的值可能发生改变,重新尝试
    }
}

简而言之,调用 shutdown 请求关闭线程池时,shutdown 首先会先检查是否有权关闭所有线程,然后再将线程池过渡到关机状态,并中断所有空闲线程(但实际上线程是否被中断要看被请求中断的线程如何回应中断请求),再启动关机钩,最后再调用 tryTerminate 方法尝试终止线程池。

进入关机状态后,线程池中仍在执行任务的线程会继续执行剩余任务。任务执行完毕调用 getTask 获取任务时发现线程池已关闭且无任何任务时,自减线程数后返回空任务,再调用 processWorkerExit 销毁线程。这样,线程逐步被销毁,线程数逐渐减为 0.

来到 tryTerminate 方法,若关机后仍有工作未完成,或线程池中仍有未销毁的线程,方法返回,取消终止。否则,先尝试修改线程池为整理状态,然后再修改为终止状态,并通知等待线程池终止的线程,取消线程阻塞。最后解锁,关机操作执行完毕。

posted @ 2023-10-04 16:54  Zebt  阅读(62)  评论(0)    收藏  举报