深入解析ThreadPoolExecutor设计精髓

如何设计一个线程池

线程池的设计与内存管理在理念上存在一个关键差异,理解这个差异有助于我们掌握线程池的核心机制。通常情况下,一个线程执行完其 run() 方法后,其生命周期就会结束。线程池需要解决的核心问题之一,就是如何实现线程的复用,避免线程在完成一次任务后即被销毁。

解决方案在于对线程 run() 方法的结构进行调整。线程池中的工作线程,其核心逻辑被置于一个循环结构内。这个循环的持续与否,由线程池的策略(例如,线程池关闭、线程空闲超时等)控制。只有当线程跳出此循环时,其生命周期才真正结束并被回收;否则,只要线程处于循环中,它就能持续接收并执行新的任务。

然而,持续运行的循环会引出新的问题:如果当前没有任务需要执行,线程会持续占用CPU资源。因此,需要一种机制来确保:当任务到来时,线程池中的线程能够高效执行;当任务队列为空时,这些线程应暂停执行,进入阻塞状态,以释放CPU资源,直到新的任务到达并将其唤醒。

这种需求引导我们采用经典的并发设计模式——生产者-消费者模型。在该模型中,任务提交方扮演“生产者”的角色,工作线程扮演“消费者”的角色,它们之间通过一个共享的缓冲区——通常是阻塞队列( BlockingQueue )——进行交互。

综上所述,线程池的核心机制可以概括为:通过 循环结构实现线程的复用 ,并借助 阻塞队列来管理线程在无任务时的状态,避免CPU空耗 ,同时实现任务的异步处理和高效调度。

我们常用的JUC( java.util.concurrent )中的线程池(如 ThreadPoolExecutor ),正是在这一核心机制的基础上,构建出的一套 高度可配置、可管理且功能完备的并发执行框架 。JUC针对并发编程中的各种复杂场景和具体需求,提供了精细化的控制和管理能力:

  • - 如何智慧地平衡系统资源占用与任务响应速度(通过 corePoolSize , maximumPoolSize , keepAliveTime 等参数的精妙组合)?
  • - 面对不同特性的任务积压(例如,任务量波动大、任务执行时间长短不一),如何选择最合适的队列策略(如 LinkedBlockingQueue , ArrayBlockingQueue , SynchronousQueue )?
  • - 当系统处理能力达到极限,任务无法被及时接纳时,如何优雅地实施拒绝策略( RejectedExecutionHandler ),以保护系统不被压垮?
  • - 如何赋予开发者定制线程创建细节的能力(通过 ThreadFactory ,例如命名线程、设置守护状态等)?
  • - 如何为线程池这一“服务”本身提供清晰、安全的启动与关闭机制(完善的生命周期管理,如 shutdown() , shutdownNow() )?
  • - 如何优雅地处理那些需要返回结果或可能需要中途取消的任务(通过 Future 模式)?
     

如果想从头开始设计一个线程池,可以考虑以下步骤和组件:

  1. 任务队列 (Task Queue):

    • 需要一个地方存放待执行的任务。BlockingQueue 是理想选择,它能处理生产者 (提交任务方) 和消费者 (工作线程) 之间的同步。
    • 决策点: 队列类型 (有界/无界/同步移交)。
  2. 工作线程 (Worker Threads):

    • 这些线程负责从队列中取出任务并执行。
    • 每个工作线程的核心逻辑是一个循环:
      • 从队列获取任务 (如果队列为空,则阻塞)。
      • 执行任务。
      • 重复。
    • 决策点: 如何创建这些线程?(ThreadFactory 是个好主意)。
    • 决策点: 需要多少工作线程?(引出 corePoolSizemaximumPoolSize 的概念)。
  3. 线程池管理器 (Pool Manager):

    • 状态管理: 线程池需要有自己的状态 (例如:运行中、关闭中、已停止、已终止)。可以用枚举或整型常量表示。
    • 任务提交 (execute 方法):
      • 如果池是运行状态:
        • 如果当前工作线程数 < corePoolSize,创建一个新工作线程。
        • 否则,尝试将任务加入队列。
        • 如果队列已满且工作线程数 < maximumPoolSize,创建一个新工作线程。
        • 如果队列已满且已达 maximumPoolSize,执行拒绝策略。
      • 如果池不是运行状态,直接拒绝。
    • 线程生命周期管理:
      • 何时创建线程 (如上所述)。
      • 何时销毁线程 (例如,空闲线程超过 corePoolSize 且存活时间达到 keepAliveTime)。
    • 关闭机制:
      • shutdown(): 停止接收新任务,等待已提交任务完成。改变池状态。
      • shutdownNow(): 停止接收新任务,尝试中断正在运行的任务,清空任务队列。改变池状态。
    • 追踪: 记录活动的工作线程数量。
  4. 并发控制:

    • 访问共享状态 (如工作线程数、池状态、工作线程集合) 需要同步。AtomicInteger 用于计数和简单状态,ReentrantLock 用于更复杂的临界区。
    • BlockingQueue 本身处理其内部的同步。
  5. 配置参数:

    • corePoolSizemaximumPoolSizekeepAliveTime, 队列类型/容量, ThreadFactoryRejectedExecutionHandler

一个极简的结构草图可能像这样 (概念性的,省略大量细节):

// 简化概念草图
class MySimpleThreadPool {
    private final BlockingQueue taskQueue;
    private final List workers = new ArrayList<>(); // 简化,实际用Set更好
    private volatile int currentPoolSize; // 简化,实际TPE用ctl
    private final int corePoolSize;
    private final int maximumPoolSize;
    private final ThreadFactory threadFactory;
    private volatile boolean isShutdown = false;

    // 构造函数...

    public void execute(Runnable command) {
        if (isShutdown) { /*拒绝*/ return; }

        if (currentPoolSize < corePoolSize) {
            if (addWorker(command)) return;
        }

        if (taskQueue.offer(command)) {
            // 可能需要检查是否有worker在运行,如果没有则添加一个
            if (currentPoolSize == 0) addWorker(null);
        } else {
            if (currentPoolSize < maximumPoolSize) {
                if (addWorker(command)) return;
            }
            /*拒绝*/
        }
    }

    private boolean addWorker(Runnable firstTask) {
        // 同步控制 currentPoolSize 和 workers 列表
        // ...
        Worker worker = new Worker(firstTask);
        Thread thread = threadFactory.newThread(worker);
        // workers.add(worker);
        // currentPoolSize++;
        thread.start();
        return true;
    }

    private class Worker implements Runnable {
        Runnable firstTask;
        public Worker(Runnable firstTask) { this.firstTask = firstTask; }

        @Override
        public void run() {
            Runnable task = firstTask;
            this.firstTask = null;
            try {
                while (task != null || (task = getTaskFromQueue()) != null) {
                    task.run();
                    task = null;
                }
            } finally {
                // worker退出处理,减少currentPoolSize,从workers列表移除
            }
        }

        private Runnable getTaskFromQueue() {
            // 从 taskQueue 获取任务,处理超时和中断
            try {
                return taskQueue.take(); // 简化,实际需要考虑keepAliveTime
            } catch (InterruptedException e) {
                return null; // 退出
            }
        }
    }

    // shutdown(), shutdownNow() 等方法...
}

这个草图省略了 ThreadPoolExecutor 中许多复杂的细节,比如 ctl 的精妙设计、Worker 继承 AQS 实现的锁机制、精确的 tryTerminate 逻辑、各种边界条件处理等。但它能帮你理解构建一个线程池的基本模块。

ThreadPoolExecutor 的设计是并发编程领域的一个典范,它在健壮性、灵活性和性能之间取得了很好的平衡。通过深入学习它的源码,可以学到很多高级并发控制技巧和设计模式。

ThreadPoolExecutor

ThreadPoolExecutor 是 java.util.concurrent 包下的一个类,它实现了 ExecutorService 接口,用于管理一个线程池,执行提交的异步任务。我们来一步步深入分析它的设计:

核心设计目标

  • 提高性能:通过重用已存在的线程来执行任务,减少了线程创建和销毁的开销。
  • 资源管理:可以控制并发线程的数量,防止因无限制创建线程而耗尽系统资源。
  • 功能扩展:提供了丰富的配置参数和钩子方法,方便用户根据需求进行定制。

关键组成部分和概念

ThreadPoolExecutor 的构造函数通常接收以下几个核心参数,这些参数也揭示了其核心设计:

  • corePoolSize (核心线程数):线程池中保持活动状态的最小线程数,即使它们处于空闲状态。除非设置了 allowCoreThreadTimeOut
  • maximumPoolSize (最大线程数):线程池中允许存在的最大线程数。
  • keepAliveTime (线程存活时间):当线程池中的线程数量超过 corePoolSize 时,多余的空闲线程在被终止前等待新任务的最长时间。
  • unit (存活时间单位)keepAliveTime 的时间单位。
  • workQueue (工作队列)BlockingQueue 类型,用于存储等待执行的任务。常见的队列类型有:
    • SynchronousQueue:一个不存储元素的阻塞队列【实际上只是会直接消费,如果不能消费,依旧会存储节点】,每个插入操作必须等待一个相应的删除操作,反之亦然。通常需要 maximumPoolSize 设置为较大值 (如 Integer.MAX_VALUE)。
    • LinkedBlockingQueue:一个基于链表结构的阻塞队列,容量可以选择有界或无界 (默认 Integer.MAX_VALUE)。两把锁分别控制生产和消费。
    • ArrayBlockingQueue:一个基于数组结构的有界阻塞队列,创建时必须指定容量。
  • threadFactory (线程工厂)ThreadFactory 接口的实现,用于创建新的工作线程。可以自定义线程的名称、优先级、是否为守护线程等。
  • handler (拒绝策略)RejectedExecutionHandler 接口的实现,当任务无法被线程池接收时 (例如队列已满且达到最大线程数,或线程池已关闭),会调用此处理器。JDK 提供了几种预定义的策略:
    • AbortPolicy (默认):抛出 RejectedExecutionException
    • CallerRunsPolicy:由提交任务的线程自己来执行该任务。
    • DiscardPolicy:直接丢弃任务。
    • DiscardOldestPolicy:丢弃队列头部的任务,然后重新尝试提交当前任务。

内部状态管理 (ctl 变量)

这是 ThreadPoolExecutor 中非常精妙的一个设计。它使用一个 AtomicInteger 类型的变量 ctl 来同时表示线程池的运行状态 (runState) 和 工作线程数量 (workerCount)

  • ctl 是一个32位的整数。
  • 高3位 ( Integer.SIZE - 3 ) 用于存储 runState
  • 低29位 ( (1 << COUNT_BITS) - 1 ) 用于存储 workerCount (大约5亿个线程,足够用了)。

运行状态 (runState) 有以下几种,并且数值上是单调递增的:

  1. RUNNING: (-1 << COUNT_BITS) 接收新任务,并处理队列中的任务。
  2. SHUTDOWN: (0 << COUNT_BITS) 不接收新任务,但处理队列中的任务。调用 shutdown() 后进入此状态。
  3. STOP: (1 << COUNT_BITS) 不接收新任务,不处理队列中的任务,并中断正在执行的任务。调用 shutdownNow() 后进入此状态。
  4. TIDYING: (2 << COUNT_BITS) 所有任务都已终止,workerCount 为0,线程池即将进入 TERMINATED 状态,此时会执行 terminated() 钩子方法。
  5. TERMINATED: (3 << COUNT_BITS) terminated() 方法执行完毕。

通过位运算,可以从 ctl 中分别获取 runState 和 workerCount

  • runStateOf(int c)c & ~COUNT_MASK
  • workerCountOf(int c)c & COUNT_MASK
  • ctlOf(int rs, int wc)rs | wc (合并状态和数量)

使用单个 AtomicInteger 来管理这两个值,可以原子地更新它们,避免了使用多个锁或多个原子变量带来的复杂性和开销。

Worker 内部类

private final class Worker extends AbstractQueuedSynchronizer implements Runnable

  • Worker 是 ThreadPoolExecutor 的一个内部类,它代表了一个实际执行任务的工作线程。
  • 它继承了 AbstractQueuedSynchronizer (AQS),并实现了一个简单的不可重入锁。这个锁用于在执行任务期间保护任务不被池中断(例如,池中断空闲worker时,不希望中断正在执行任务的worker)。
    • setState(-1) 初始化时抑制中断,直到 runWorker 中调用 w.unlock()
    • lock() / unlock() / isLocked() 等方法控制任务执行期间的锁定状态。
  • 它实现了 Runnable 接口,其 run() 方法会调用外部 ThreadPoolExecutor 的 runWorker(this) 方法。
  • 每个 Worker 对象持有一个 Thread 对象 (通过 threadFactory 创建) 和一个 firstTask (初始任务,可以为 null)。

Worker是怎么利用AQS的

ThreadPoolExecutor.Worker 是如何设置状态、重写 tryRelease 以及如何利用 AQS 的。

Worker 类继承了 AbstractQueuedSynchronizer,并将其用作一个简单的、非可重入的互斥锁。AQS 的核心是维护一个同步状态(一个 int 类型的 state 变量)以及一个等待线程队列。

基本没有利用这个等待线程的队列,控制中断才用到队列。

让我们一步步分析 Worker 中与 AQS 相关的实现:

1. Worker 的构造函数与初始状态设置

ThreadPoolExecutor.java

// ...
    private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable
    {
        // ...
        /** Thread this worker is running in.  Null if factory fails. */
        @SuppressWarnings("serial") // Unlikely to be serializable
        final Thread thread;
        /** Initial task to run.  Possibly null. */
        @SuppressWarnings("serial") // Not statically typed as Serializable
        Runnable firstTask;
        // ...

        /**
         * Creates with given first task and thread from ThreadFactory.
         * @param firstTask the first task (null if none)
         */
        Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }
// ...
  • setState(-1); // inhibit interrupts until runWorker:
    • 在 Worker 对象被创建时,它的构造函数做的第一件事就是调用 setState(-1)。这是 AQS 提供的方法,用于设置同步状态的值。
    • 这里将状态设置为 -1 是一个特殊用途:在 Worker 线程真正开始执行 runWorker 方法中的任务循环之前,抑制对该线程的中断。
    • 稍后会看到,在 interruptIfStarted() 方法中,会检查 getState() >= 0。因此,只要状态是 -1interruptIfStarted() 就不会执行中断操作。

2. runWorker 方法与状态的转变

当 Worker 线程启动后,它会执行其 run() 方法,该方法直接调用外部类 ThreadPoolExecutor 的 runWorker(this) 方法。

ThreadPoolExecutor.java

// ...
    final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
            while (task != null || (task = getTask()) != null) {
                w.lock();
                // If pool is stopping, ensure thread is interrupted;
                // if not, ensure thread is not interrupted.  This
                // requires a recheck in second case to deal with
// ...
  • w.unlock(); // allow interrupts:
    • 在 runWorker 方法的开头,执行 w.firstTask = null; 之后,紧接着就是 w.unlock()
    • 这里的 unlock() 会调用到 Worker 重写的 tryRelease(1) 方法( unlock() 内部调用 release(1),而 release(1) 会调用 tryRelease(1))。

3. tryRelease(int unused) 方法的实现

ThreadPoolExecutor.java

// ...
        // Lock methods
        //
        // The value 0 represents the unlocked state.
        // The value 1 represents the locked state.

        protected boolean isHeldExclusively() {
            return getState() != 0;
        }

        protected boolean tryAcquire(int unused) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        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(); }
// ...
  • protected boolean tryRelease(int unused):
    • setExclusiveOwnerThread(null);: 首先,它清除了当前持有锁的线程。AQS 内部使用 exclusiveOwnerThread 字段来记录当前独占锁的线程。释放锁时,自然要将其置为 null
    • setState(0);这是关键点。它将同步状态设置为 0。按照 Worker 中锁的约定,0 代表锁是未锁定状态。
    • return true;: 表示释放操作成功。
    • 所以,当 runWorker 开始时调用 w.unlock(),实际上是将 Worker 的 AQS 状态从初始的 -1 变成了 0。这意味着此时 Worker 线程已经准备好接受任务,并且不再抑制中断了(因为 getState() >= 0 现在为真)。

Worker 类自身重写的方法(如 isHeldExclusively)没有直接去查询 getExclusiveOwnerThread() 的结果来做判断,但设置这个属性是遵循 AQS 框架设计的一部分,使用条件变量需要使用这个方法

getActiveCount() 最终会调用isHeldExclusively(),因此提供的是一个近似值(-1 != 0),这种计算方式简化了逻辑,并且考虑到状态 -1 的短暂性,这种近似在多数情况下是可接受的。它反映了那些“不是空闲状态”的 Worker 数量。

4. tryAcquire(int unused) 方法的实现

当 Worker 线程要执行一个任务之前,它会调用 w.lock()

  • public void lock() { acquire(1); }Worker 的 lock() 方法直接调用了 AQS 的 acquire(1) 模板方法。

  • AQS 的 acquire(int arg) 方法会首先尝试调用子类重写的 tryAcquire(arg)。如果 tryAcquire 返回 true(表示获取锁成功),则 acquire 方法直接返回。如果 tryAcquire 返回 false,则 AQS 会将当前线程加入等待队列并可能挂起。

  • protected boolean tryAcquire(int unused):

    • if (compareAndSetState(0, 1)): 这是获取锁的核心。它使用 AQS 提供的 compareAndSetState (CAS) 原子操作。
      • 它检查当前状态是否为 0(未锁定)。
      • 如果是 0,则尝试原子地将其更新为 1(锁定状态)。
    • 如果 CAS 操作成功(即成功将状态从 0 变为 1):
      • setExclusiveOwnerThread(Thread.currentThread());: 将当前线程设置为锁的独占所有者。
      • return true;: 表示获取锁成功。
    • 如果 CAS 操作失败(意味着其他线程已经获取了锁,或者状态不是 0),则 return false;

5. interruptIfStarted() 方法

ThreadPoolExecutor.java

// ...
        void interruptIfStarted() {
            Thread t;
            if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
                try {
                    t.interrupt();
                } catch (SecurityException ignore) {
                }
            }
        }
// ...
  • if (getState() >= 0 ...): 这个检查确保只有当 Worker 的状态不再是初始的 -1 时(即 runWorker 中的 unlock() 已经被调用,状态变为 0 或 1),才尝试中断 Worker 线程。这防止了在 Worker 线程完全初始化并准备好运行任务之前就被中断。

总结 Worker 如何利用 AQS:

  1. 状态表示:
    • -1: 初始状态,用于在 Worker 线程启动早期抑制中断。
    • 0: 锁未被持有(解锁状态)。
    • 1: 锁已被持有(锁定状态)。
  2. 非可重入锁Worker 实现的 tryAcquire 逻辑非常简单,它不检查当前线程是否已经是锁的持有者,因此它是一个非可重入锁。如果一个线程已经持有了锁(状态为 1),再次调用 lock() 会因为 compareAndSetState(0, 1) 失败而进入 AQS 的等待队列(或者 tryLock() 直接返回 false)。
  3. 独占模式Worker 使用 AQS 的独占模式。setExclusiveOwnerThread 和 getExclusiveOwnerThread (间接通过 isHeldExclusively 判断) 用于此目的。
  4. 原子状态转换: 通过 compareAndSetState 实现无锁的原子状态更新,这是 AQS 的核心优势之一,用于高效地获取锁。
  5. 中断控制: 通过巧妙地利用初始状态 -1 和 getState() >= 0 的检查,实现了对 Worker 线程启动初期中断的精细控制。
  6. 任务执行保护: 在 runWorker 的循环中,每次执行任务前调用 w.lock(),任务执行后在 finally 块中调用 w.unlock()。这确保了任务执行期间,Worker 持有锁,防止了来自线程池管理操作(如 shutdownNow)对正在执行任务的线程的意外中断。当 interruptIdleWorkers 尝试中断空闲 worker 时,它会先 w.tryLock(),如果 worker 正在执行任务(已持有锁),tryLock 会失败,从而不会中断正在忙碌的 worker。

通过这种方式,ThreadPoolExecutor.Worker 利用 AQS 的基本同步原语,以非常轻量级和高效的方式实现了一个满足其特定需求的非可重入锁和中断控制机制。

为什么 Worker 需要继承 AQS

ThreadPoolExecutor 中的 Worker 类扩展了AQS,主要是为了实现一个简单的、非可重入的互斥锁ThreadPoolExecutor 的作者特意选择实现一个非可重入锁,而不是直接使用 ReentrantLock。注释中提到:

  • ThreadPoolExecutor.java

    // ... We implement a simple
    // non-reentrant mutual exclusion lock rather than use
    // ReentrantLock because we do not want worker tasks to be able to
    // reacquire the lock when they invoke pool control methods like
    // setCorePoolSize. ...
    
  • 这意味着如果一个正在执行的任务(它已经间接持有了 Worker 的锁)尝试调用线程池的某些控制方法(如 setCorePoolSize),而这些方法可能又需要获取其他锁或操作 Worker 状态,使用非可重入锁可以避免复杂的重入问题或潜在的死锁。任务本身不应该能够重新获取它所在的 Worker 线程已经为它持有的锁。

Worker 使用了 AQS 的哪些能力?

  • getState() / setState() / compareAndSetState(): 用于管理锁的状态(-1 表示初始抑制中断,0 表示未锁定,1 表示已锁定)。
  • tryAcquire(int unused): 实现获取锁的逻辑。如果状态为0(未锁定),则通过CAS操作将其设置为1(锁定),并设置当前线程为独占所有者。
  • tryRelease(int unused): 实现释放锁的逻辑。清除独占所有者线程,并将状态设置为0。
  • isHeldExclusively(): 判断当前线程是否持有锁。
  • 它还提供了 lock()tryLock()unlock()isLocked() 这些标准的锁接口方法,这些方法内部委托给了AQS的 acquire(1) 和 release(1) 等模板方法,这些模板方法又会回调到 Worker 自己实现的 tryAcquire 和 tryRelease

直接使用锁(比如 ReentrantLock)可以吗?

理论上,可以使用 ReentrantLock 来实现互斥。但是,正如上面提到的,设计者有特定的需求:

  1. 非可重入性ReentrantLock 是可重入的。如果需要严格的非可重入行为,以避免工作任务内部调用线程池方法时发生锁的重入,那么自定义一个非可重入锁是更直接的选择。
  2. 启动时的中断抑制:通过将AQS状态初始化为-1并在 runWorker 开始时通过 unlock()(实际是将状态置为0)来解除这种抑制,这是一种精巧的控制方式。用 ReentrantLock 实现完全相同的启动时中断控制逻辑可能会稍微复杂一些,或者说不如直接操纵AQS的状态来得直接。
  3. 轻量级:对于 Worker 这种内部辅助类,直接继承AQS实现一个极简的锁,可以避免引入 ReentrantLock 相对更重的对象(尽管现代JVM优化后差异可能不大,但在并发库的设计中,这种考量很常见)。

总结来说,Worker 继承 AQS 是为了以一种轻量级且高度可控的方式实现一个非可重入锁,这个锁对于保护任务执行和精细控制 Worker 线程的中断状态至关重要。虽然可以使用 ReentrantLock,但AQS提供了更大的灵活性来实现这些特定的语义。

核心执行流程 execute(Runnable command)

这是向线程池提交任务的入口。其逻辑大致如下:

  1. 检查核心线程

    • 获取当前 ctl 值,计算 workerCountOf(c)
    • 如果 workerCount < corePoolSize,尝试调用 addWorker(command, true) 创建一个新的核心线程来执行任务。如果成功,则返回。
    • 如果 addWorker 失败 (可能因为并发修改 ctl 或线程工厂创建失败),重新获取 ctl
  2. 尝试入队

    • 如果线程池处于 RUNNING 状态,并且 workQueue.offer(command) 成功(任务成功加入队列):
      • 再次检查 ctl (recheck)。如果线程池不再是 RUNNING 状态 (例如,在入队操作期间被关闭),并且能成功从队列中移除该任务 (remove(command)),则拒绝该任务 (reject(command))。
      • 如果线程池仍在运行,但 workerCountOf(recheck) == 0 (可能所有线程都意外死掉了),则尝试启动一个新的非核心线程 (addWorker(null, false)) 来处理队列中的任务 (但不携带新提交的 command,因为 command 已经在队列里了)。
  3. 尝试创建非核心线程

    • 如果无法入队 (例如队列已满),则尝试调用 addWorker(command, false) 创建一个新的非核心线程 (使用 maximumPoolSize 作为上限)。
    • 如果 addWorker 成功,则返回。
  4. 拒绝任务

    • 如果以上步骤都失败 (例如,workerCount >= maximumPoolSize 且队列已满,或者线程池已关闭),则调用 reject(command) 执行拒绝策略。

addWorker(Runnable firstTask, boolean core) 方法

这个方法负责创建并启动一个新的 Worker

  1. 循环和CAS:使用一个 retry 标签和内部循环来处理并发。

    • 检查线程池状态:如果不是 RUNNING 状态,并且满足特定条件(如 STOP 状态,或 firstTask != null,或队列为空),则不能添加 worker,返回 false
    • 检查 worker 数量:如果 workerCount 已经达到上限(corePoolSize 或 maximumPoolSize,取决于 core 参数),返回 false
    • CAS 增加 workerCount:通过 ctl.compareAndSet(expect, expect + 1) (即 compareAndIncrementWorkerCount) 尝试原子地增加 workerCount。如果成功,跳出 retry 循环。
    • 如果 CAS 失败,说明 ctl 被其他线程修改了,重新读取 ctl。如果状态改变,可能需要回到外层 retry 循环;否则,仅在内层循环重试 CAS。
  2. 创建 Worker 和 Thread

    • w = new Worker(firstTask):创建一个 Worker 对象。Worker 的构造函数会通过 threadFactory.newThread(this) 创建一个新线程,this 指的是 Worker 实例本身 (因为 Worker实现了 Runnable)。
    • 获取 w.thread
  3. 加锁并注册 Worker

    • 获取 mainLock。这是为了保护 workers 集合和 largestPoolSize【记录线程池生命周期内曾经达到的最大工作线程数】 等共享数据。
    • 在锁内再次检查线程池状态。如果线程池已关闭且 firstTask 为 null (不允许在关闭后添加空闲线程),则回滚。
    • 如果线程状态不是 NEW,抛异常。
    • workers.add(w):将新 Worker 添加到 workers 集合中。
    • 更新 largestPoolSize
    • 释放 mainLock
  4. 启动线程

    • 如果 workerAdded 为 true,则调用 container.start(t) (在较新JDK中,通过 SharedThreadContainer 管理,旧版直接 t.start()) 启动线程。
    • 设置 workerStarted = true
  5. 失败处理

    • 如果在任何步骤失败 (例如 threadFactory 返回 null,或启动线程时发生 OutOfMemoryError),并且 workerStarted 为 false,则调用 addWorkerFailed(w) 进行回滚操作(从 workers 移除,递减 workerCount,尝试终止线程池)。
  6. 返回 workerStarted

runWorker(Worker w) 方法

这是工作线程的主循环,在 Worker.run() 中被调用。

  1. 获取当前线程 wt,获取 Worker w 的 firstTask
  2. w.firstTask = null; (防止任务被重复执行)
  3. w.unlock(); // 允许中断,因为 Worker 初始化时 state 为 -1 (抑制中断)
  4. 主循环while (task != null || (task = getTask()) != null)
    • 如果 task 不为 null (初始任务或从队列获取的任务),则执行它。
    • w.lock(); // 在执行任务前获取 Worker 自己的锁,防止任务被池中断。
    • 中断检查:如果池正在停止 (STOP),或者线程被中断且池正在停止,确保工作线程被中断。
    • beforeExecute(wt, task); // 执行前置钩子方法。
    • 执行任务
       
      try {
          task.run();
          afterExecute(task, null); // 正常完成
      } catch (Throwable ex) {
          afterExecute(task, ex);   // 异常完成
          throw ex;                 // 抛出异常,会导致 completedAbruptly = true
      } finally {
          task = null;              // 清理当前任务引用
          w.completedTasks++;       // 增加 Worker 的完成任务数
          w.unlock();               // 释放 Worker 锁
      }
      
  5. 循环结束:当 getTask() 返回 null 时,表示 worker 需要退出。
  6. completedAbruptly 标记任务是否因异常退出循环。
  7. 善后处理finally { processWorkerExit(w, completedAbruptly); }

getTask() 方法

此方法负责从工作队列中获取任务,并处理 worker 的生命周期。

  1. 循环:不断尝试获取任务或决定是否退出。
  2. 获取 ctl 值。
  3. 检查退出条件
    • 如果线程池状态 >= SHUTDOWN,并且 (状态 >= STOP 或 workQueue.isEmpty()),则 worker 必须退出。调用 decrementWorkerCount() 并返回 null
  4. 获取 workerCount (wc)。
  5. 判断是否需要超时等待 (timed):
    • boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
  6. 检查是否因超时或池缩小而退出
    • 如果 (wc > maximumPoolSize (池动态缩小了) 或 (timed && timedOut (上次poll超时了)))
    • 并且 (wc > 1 或 workQueue.isEmpty() (如果这是最后一个线程且队列不空,则不能退出))
    • 则尝试 compareAndDecrementWorkerCount(c),如果成功,返回 null。否则继续循环。
  7. 从队列获取任务
    • 如果 timed 为 true,调用 workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS)
    • 否则,调用 workQueue.take() (阻塞等待)。
    • 如果获取到任务 r,返回 r
    • 如果 poll 超时返回 null,设置 timedOut = true
    • 如果发生 InterruptedException,重置 timedOut = false 并重试。

关闭流程 (shutdown()shutdownNow()tryTerminate())

  • shutdown():

    • 获取 mainLock
    • checkShutdownAccess(): 检查权限。
    • advanceRunState(SHUTDOWN): 将状态推进到 SHUTDOWN
    • interruptIdleWorkers(): 中断所有空闲的 worker。
    • onShutdown(): 钩子方法 (主要给 ScheduledThreadPoolExecutor 用)。
    • 释放 mainLock
    • tryTerminate(): 尝试终止线程池。
  • shutdownNow():

    • 获取 mainLock
    • checkShutdownAccess(): 检查权限。
    • advanceRunState(STOP): 将状态推进到 STOP
    • interruptWorkers(): 中断所有 worker (包括正在执行任务的)。
    • tasks = drainQueue(): 排空工作队列,返回未执行的任务列表。
    • 释放 mainLock
    • tryTerminate(): 尝试终止线程池。
    • 返回 tasks
  • tryTerminate():

    • 循环检查 ctl 状态。
    • 如果池是 RUNNING,或已达到 TIDYING,或 (SHUTDOWN 状态但队列不为空),则直接返回。
    • 如果 workerCount != 0,说明还有 worker 存活,中断一个空闲 worker (interruptIdleWorkers(ONLY_ONE)) 以确保关闭信号传播,然后返回。
    • 如果 workerCount == 0 (并且满足关闭条件):
      • 获取 mainLock
      • CAS 尝试将状态设置为 TIDYING
      • 如果成功:
        • terminated(); // 执行终止钩子方法。
        • ctl.set(ctlOf(TERMINATED, 0)); // 设置状态为 TERMINATED。
        • termination.signalAll(); // 唤醒所有在 awaitTermination() 中等待的线程。
        • container.close(); // 关闭线程容器。
      • 释放 mainLock

钩子方法

  • protected void beforeExecute(Thread t, Runnable r): 任务执行前调用。
  • protected void afterExecute(Runnable r, Throwable t): 任务执行后调用 (无论正常结束还是异常结束)。
  • protected void terminated(): 线程池完全终止后调用。

这些方法默认是空实现,子类可以重写它们来添加自定义逻辑,如日志记录、性能统计、资源清理等。

什么时候可以使用 CAS,什么时候必须加全局锁?

1. 可以使用 CAS 的情况:

  • 对单个共享变量的原子更新:这是 CAS 最典型的应用场景。例如,原子地增加一个计数器、原子地更新一个状态标志、或者原子地设置一个引用。
    • 在 ThreadPoolExecutor 中,ctl (一个 AtomicInteger) 的更新就是通过 CAS 实现的。ctl巧妙地将运行状态 (runState) 和工作线程数 (workerCount) 打包到单个整数中。
      • compareAndIncrementWorkerCount(int expect)ctl.compareAndSet(expect, expect + 1);
      • compareAndDecrementWorkerCount(int expect)ctl.compareAndSet(expect, expect - 1);
      • 在 tryTerminate() 中: ctl.compareAndSet(c, ctlOf(TIDYING, 0))
      • 在 advanceRunState() 中: ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c)))
  • 临界区非常小且简单:如果需要保护的操作非常简短,并且只涉及一个变量的读-改-写,CAS 可能是个好选择。
  • 竞争不激烈或可接受自旋:如果预期并发冲突不多,或者短暂的自旋是可以接受的。

2. 必须使用全局锁的情况:

  • 需要保证多个操作的原子性 (复合操作):当一个逻辑单元需要修改多个共享变量,或者执行一系列必须不被打断的操作时,通常需要全局锁。

    • 在 ThreadPoolExecutor 中,mainLock 保护了对 workers (一个 HashSet)、largestPoolSize 和 completedTaskCount 的访问。
    • 例如,在 addWorker() 方法中,将新的 Worker 添加到 workers 集合,并可能更新 largestPoolSize,这两个操作需要作为一个原子单元执行:

      final ReentrantLock mainLock = this.mainLock;
      mainLock.lock();
      try {
          // ...
          workers.add(w);
          workerAdded = true;
          int s = workers.size();
          if (s > largestPoolSize)
              largestPoolSize = s;
          // ...
      } finally {
          mainLock.unlock();
      }
      
      这里无法用单个 CAS 操作同时完成对 workers 集合的修改和对 largestPoolSize 的条件更新。
  • 保护复杂的数据结构:标准库中的很多集合类 (如 HashSetArrayList) 本身不是线程安全的。对它们的并发修改需要外部同步,通常通过锁来实现。

    • workers 是一个 HashSet,它的添加、删除、迭代操作都需要在 mainLock 的保护下进行。
  • 需要使用 Condition 变量进行复杂的线程协作Condition 必须与 Lock 配合使用,以实现更灵活的等待/通知机制。

    • ThreadPoolExecutor 中的 termination 条件变量 (mainLock.newCondition()) 用于 awaitTermination() 方法,允许线程等待线程池终止。
  • 避免“中断风暴”等特定并发问题ThreadPoolExecutor 的注释中提到:

    "While we could use a concurrent set of some sort, it turns out to be generally preferable to use a lock. Among the reasons is that this serializes interruptIdleWorkers, which avoids unnecessary interrupt storms, especially during shutdown."

    这意味着使用 mainLock 来串行化 interruptIdleWorkers 操作,可以避免在关闭期间多个线程同时尝试中断其他线程,从而引发不必要的混乱。

  • 代码的清晰性和可维护性:对于复杂的同步逻辑,使用锁通常比尝试用一系列复杂的 CAS 操作来实现更容易理解和维护。

mainlock 注释

深入分析一下 ThreadPoolExecutor 中关于 mainLock 的这段注释。

ThreadPoolExecutor.java

// ... existing code ...
    /**
     * Lock held on access to workers set and related bookkeeping.
     * While we could use a concurrent set of some sort, it turns out
     * to be generally preferable to use a lock. Among the reasons is
     * that this serializes interruptIdleWorkers, which avoids
     * unnecessary interrupt storms, especially during shutdown.
     * Otherwise exiting threads would concurrently interrupt those
     * that have not yet interrupted. It also simplifies some of the
     * associated statistics bookkeeping of largestPoolSize etc. We
     * also hold mainLock on shutdown and shutdownNow, for the sake of
     * ensuring workers set is stable while separately checking
     * permission to interrupt and actually interrupting.
     */
    private final ReentrantLock mainLock = new ReentrantLock();
// ... existing code ...

这段注释解释了为什么选择使用 ReentrantLock (即 mainLock) 而不是某种并发集合 (concurrent set) 来保护对 workers 集合以及相关簿记(bookkeeping)数据的访问。我们可以逐句解读:

  1. Lock held on access to workers set and related bookkeeping.

    • 核心目的mainLock 的首要职责是保护共享资源。
      • workers:这是一个 HashSet,存储了线程池中所有的工作线程。HashSet 本身不是线程安全的,并发地添加或删除元素会导致问题。
      • related bookkeeping:指的是像 largestPoolSize (记录线程池曾经达到的最大线程数) 和 completedTaskCount (已完成任务总数) 这样的统计数据。这些数据的更新也需要同步。
  2. While we could use a concurrent set of some sort, it turns out to be generally preferable to use a lock.

    • 承认替代方案:注释首先承认,理论上可以使用并发集合(例如 ConcurrentHashMap.newKeySet() 来创建一个并发的 Set)来管理 workers
    • 选择锁的原因:但紧接着指出,在 ThreadPoolExecutor 的具体场景下,使用锁通常是“更可取”的。接下来的几点解释了为什么。
  3. Among the reasons is that this serializes interruptIdleWorkers, which avoids unnecessary interrupt storms, especially during shutdown.

    • interruptIdleWorkers 的作用:这个方法会遍历 workers 集合,并中断那些当前空闲(即正在等待任务)的工作线程。这通常在线程池关闭、配置更改(如缩减核心线程数)或需要唤醒线程来处理状态变化时调用。
    • 什么是“中断风暴 (interrupt storms)”?:想象一下,如果没有 mainLock 进行序列化:
      • 在线程池关闭(shutdown)过程中,可能有多个机制(例如 shutdown() 方法本身,或者因任务队列变空而触发的 tryTerminate())都尝试调用 interruptIdleWorkers
      • 如果这些调用并发执行,它们可能会同时遍历 workers 集合,并对同一批空闲线程发出重复的中断信号。
      • 这不仅是多余的操作,还可能导致不必要的唤醒和竞争,尤其是在系统资源已经紧张的关闭阶段。
    • mainLock 如何解决:通过在 interruptIdleWorkers 方法内部获取 mainLock,确保了在任何时刻只有一个线程能够执行中断空闲线程的逻辑。这就将并发的“中断请求”变成了串行处理,避免了混乱和冗余。
    • “尤其是在关闭期间 (especially during shutdown)”:线程池关闭是一个敏感阶段,目标是平稳、有序地停止所有工作线程并释放资源。中断风暴会干扰这个过程,使其变得混乱和低效。通过序列化中断操作,可以确保关闭过程更加可控。
  4. Otherwise exiting threads would concurrently interrupt those that have not yet interrupted.

    • 具体场景:这是对“中断风暴”的一个更具体的解释。当一个工作线程(Worker)完成其任务并准备退出时,或者在线程池关闭流程中,它可能需要确保其他空闲线程也被适当地通知或中断。
    • 如果没有 mainLock,多个即将退出的线程可能同时尝试中断其他尚未被中断的空闲线程。这不仅低效,而且如果中断逻辑与线程状态的判断之间存在微小的时间窗口,还可能导致一些难以追踪的并发问题。
    • mainLock 确保了在检查和中断其他线程时,workers 集合的状态是稳定的,并且中断操作是有序的。
  5. It also simplifies some of the associated statistics bookkeeping of largestPoolSize etc.

    • 简化的统计簿记
      • largestPoolSize:当添加新 worker 后,需要 workers.size() 与当前的 largestPoolSize 比较并更新。这是一个典型的“读取-比较-写入”操作。如果没有锁,你需要使用 AtomicInteger 和 CAS 循环来原子地更新它,例如:do { old = largest.get(); newSize = Math.max(old, currentSize); } while (!largest.compareAndSet(old, newSize));
      • completedTaskCount:当 worker 退出时,会累加其完成的任务数到 completedTaskCount。这也是一个“读取-修改-写入”操作。
    • 锁的简化作用:虽然单个统计变量的更新可以使用 AtomicInteger 或 AtomicLong 来实现,但 mainLock 的好处在于它能原子地保护一组相关的操作。例如,在 addWorker 方法中,将 worker 添加到 workers 集合 并且 更新 largestPoolSize,这两个操作需要在同一个原子单元内完成。在 processWorkerExit 中,从 workers 移除 worker 并且 更新 completedTaskCount 也类似。使用一个 mainLock 可以简单直接地保证这些复合操作的原子性,而尝试用多个独立的 CAS 操作来协调对并发集合和多个原子统计变量的修改会复杂得多,也更容易出错。
  6. We also hold mainLock on shutdown and shutdownNow, for the sake of ensuring workers set is stable while separately checking permission to interrupt and actually interrupting.

    • shutdown() 和 shutdownNow() 中的锁:这两个方法是线程池生命周期管理的关键。它们执行的操作包括:
      1. 修改线程池的运行状态(runState,通常通过对 ctl 的 CAS 操作完成)。
      2. 检查调用者是否有权限关闭线程池和中断线程(checkShutdownAccess())。
      3. 实际中断工作线程(通过 interruptIdleWorkers() 或 interruptWorkers())。
    • workers 集合的稳定性至关重要
      • 权限检查 (checkShutdownAccess):此方法会遍历 workers 集合,对每个 worker 的线程调用 SecurityManager.checkAccess()。如果在遍历过程中 workers 集合被并发修改(添加或删除 worker),权限检查可能会基于一个不一致或过时的线程列表,导致检查不完整或不正确。
      • 实际中断:同样,当中断方法遍历 workers 集合去中断线程时,如果集合可以并发修改,可能会导致 ConcurrentModificationException(如果不是 HashSet 而是其他非并发集合),或者错过需要中断的线程,或者尝试中断已经被移除的线程。
    • mainLock 确保了在执行这些关键的关闭步骤时(特别是权限检查和中断操作),workers 集合的内容是固定的、一致的。

总结:

选择 ReentrantLock 而不是并发集合,是 ThreadPoolExecutor 设计者(Doug Lea)在权衡了正确性、代码简洁性、特定并发模式的处理(如避免中断风暴)以及复杂操作的原子性保证后做出的决定。

虽然并发集合在某些场景下能提供更好的吞吐量,但 mainLock 在这里提供了一种更粗粒度但更易于管理和推理的同步机制,有效地协调了对 workers 集合的访问、相关统计数据的更新以及关键的生命周期管理操作(如中断和关闭)。这种设计优先考虑了在复杂并发场景下的健壮性和可维护性。

如何衡量这一点?

  1. 原子性需求:首先分析操作需要达到的原子性级别。

    • 单个变量的原子性:CAS 是首选。
    • 多个变量或一系列步骤的原子性:通常需要锁。
  2. 临界区的大小和复杂度

    • 小而简单:CAS 可能适用。
    • 大而复杂:锁更合适,也更易于正确实现。
  3. 竞争的预期程度

    • 低竞争:CAS 通常性能更好。
    • 高竞争:需要权衡。CAS 的自旋可能消耗大量 CPU。锁虽然会导致阻塞,但可以让出 CPU 给其他线程。有时,高竞争下锁的性能可能由于避免了空转而更稳定。然而,高竞争的锁也可能成为瓶颈。
  4. 数据结构特性:操作的数据结构是否支持无锁操作,或者是否有现成的并发数据结构可用 (如 ConcurrentHashMapCopyOnWriteArrayList)。如果使用非线程安全的结构,则必须加锁。

  5. 是否需要高级锁特性:如条件变量、公平性、可中断等。如果需要,ReentrantLock 是自然的选择。

  6. 性能测试和分析:在实际或模拟的负载下进行性能测试,分析瓶颈所在。不要过早优化,但要对关键路径的并发控制策略有清晰的认识。

有什么方法证明某一段不能替换为 CAS 吗?

要证明一段代码不能简单地用 CAS 替换,通常需要指出该代码段的原子性需求超出了单个 CAS 操作的能力范围:

  1. 涉及多个独立的内存位置的原子更新

    • 如前所述 addWorker 中对 workers 集合和 largestPoolSize 的原子更新。这两个是不同的对象或字段,无法用一个 CAS 指令同时原子地修改。
    • 你不能设计一个 CAS 操作,它的“预期原值”是 workers 集合的某个状态 并且 largestPoolSize 的某个值,然后原子地将它们更新为新状态和新值。CAS 通常操作的是一个固定大小的内存字。
  2. 操作的逻辑依赖于一个非原子性的中间状态或计算

    • 如果一个操作包含:读取值 A -> 基于 A 计算值 B -> 读取值 C -> 基于 B 和 C 更新值 D。如果 A、C 的读取和 D 的更新之间不能有其他线程修改它们,并且这个过程不能被压缩成对单个变量的 CAS,那么就需要锁。
  3. 需要保证一系列步骤的顺序性和不可分割性

    • shutdown() 方法的步骤:advanceRunState() (内部用 CAS) -> interruptIdleWorkers() (遍历 workers,需要锁) -> onShutdown()。整个 shutdown() 的意图是作为一个整体的转换过程。虽然部分步骤可以是 CAS,但整个序列的协调和对 workers 的稳定访问需要 mainLock
  4. 对复杂数据结构的内部修改

    • 直接对 HashSet 的内部节点进行 CAS 操作来添加或删除元素是非常复杂的,并且标准 HashSet 并非为此设计。你需要一个专门为此设计的并发数据结构。如果选择不使用这类并发数据结构,而是使用如 HashSet 这样的非线程安全集合,那么对其所有访问(读和写)都必须由同一个锁来保护,以确保一致性。

总结 ThreadPoolExecutor 的实践:

  • ctl (状态和工作线程数):设计为一个 AtomicInteger,其更新逻辑(如增减 workerCount,改变 runState)被精心设计成可以通过 CAS 原子完成。这是因为状态和计数的改变可以被编码到单个整数中,并且操作相对直接。
  • workers (Worker 集合), largestPoolSizecompletedTaskCount 及相关操作 (如 shutdowninterruptIdleWorkers):这些涉及到对 HashSet 的修改、多个统计变量的更新、或者需要遍历 workers 集合并执行操作的复杂逻辑。这些操作的原子性和一致性通过 mainLock 来保证。这样做简化了设计,保证了正确性,并且如注释所说,避免了一些并发问题(如中断风暴)。

在并发设计中,正确性通常是首要考虑的。在保证正确性的前提下,再根据具体场景和性能需求来选择合适的同步机制。CAS 是一种强大的工具,但并非万能药;锁在许多情况下仍然是保证复杂操作原子性的最直接和可靠的方式。

你可能感兴趣的:(Java,数据结构,并发,java,设计模式,数据结构)