线程池的设计与内存管理在理念上存在一个关键差异,理解这个差异有助于我们掌握线程池的核心机制。通常情况下,一个线程执行完其 run() 方法后,其生命周期就会结束。线程池需要解决的核心问题之一,就是如何实现线程的复用,避免线程在完成一次任务后即被销毁。
解决方案在于对线程 run() 方法的结构进行调整。线程池中的工作线程,其核心逻辑被置于一个循环结构内。这个循环的持续与否,由线程池的策略(例如,线程池关闭、线程空闲超时等)控制。只有当线程跳出此循环时,其生命周期才真正结束并被回收;否则,只要线程处于循环中,它就能持续接收并执行新的任务。
然而,持续运行的循环会引出新的问题:如果当前没有任务需要执行,线程会持续占用CPU资源。因此,需要一种机制来确保:当任务到来时,线程池中的线程能够高效执行;当任务队列为空时,这些线程应暂停执行,进入阻塞状态,以释放CPU资源,直到新的任务到达并将其唤醒。
这种需求引导我们采用经典的并发设计模式——生产者-消费者模型。在该模型中,任务提交方扮演“生产者”的角色,工作线程扮演“消费者”的角色,它们之间通过一个共享的缓冲区——通常是阻塞队列( BlockingQueue )——进行交互。
综上所述,线程池的核心机制可以概括为:通过 循环结构实现线程的复用 ,并借助 阻塞队列来管理线程在无任务时的状态,避免CPU空耗 ,同时实现任务的异步处理和高效调度。
我们常用的JUC( java.util.concurrent )中的线程池(如 ThreadPoolExecutor ),正是在这一核心机制的基础上,构建出的一套 高度可配置、可管理且功能完备的并发执行框架 。JUC针对并发编程中的各种复杂场景和具体需求,提供了精细化的控制和管理能力:
如果想从头开始设计一个线程池,可以考虑以下步骤和组件:
任务队列 (Task Queue):
BlockingQueue
是理想选择,它能处理生产者 (提交任务方) 和消费者 (工作线程) 之间的同步。工作线程 (Worker Threads):
ThreadFactory
是个好主意)。corePoolSize
, maximumPoolSize
的概念)。线程池管理器 (Pool Manager):
execute
方法):
corePoolSize
,创建一个新工作线程。maximumPoolSize
,创建一个新工作线程。maximumPoolSize
,执行拒绝策略。corePoolSize
且存活时间达到 keepAliveTime
)。shutdown()
: 停止接收新任务,等待已提交任务完成。改变池状态。shutdownNow()
: 停止接收新任务,尝试中断正在运行的任务,清空任务队列。改变池状态。并发控制:
AtomicInteger
用于计数和简单状态,ReentrantLock
用于更复杂的临界区。BlockingQueue
本身处理其内部的同步。配置参数:
corePoolSize
, maximumPoolSize
, keepAliveTime
, 队列类型/容量, ThreadFactory
, RejectedExecutionHandler
。一个极简的结构草图可能像这样 (概念性的,省略大量细节):
// 简化概念草图
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位的整数。Integer.SIZE - 3
) 用于存储 runState
。(1 << COUNT_BITS) - 1
) 用于存储 workerCount
(大约5亿个线程,足够用了)。运行状态 (runState) 有以下几种,并且数值上是单调递增的:
RUNNING
: (-1 << COUNT_BITS) 接收新任务,并处理队列中的任务。SHUTDOWN
: (0 << COUNT_BITS) 不接收新任务,但处理队列中的任务。调用 shutdown()
后进入此状态。STOP
: (1 << COUNT_BITS) 不接收新任务,不处理队列中的任务,并中断正在执行的任务。调用 shutdownNow()
后进入此状态。TIDYING
: (2 << COUNT_BITS) 所有任务都已终止,workerCount
为0,线程池即将进入 TERMINATED
状态,此时会执行 terminated()
钩子方法。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
)。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
。因此,只要状态是 -1
,interruptIfStarted()
就不会执行中断操作。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
(锁定状态)。0
变为 1
):
setExclusiveOwnerThread(Thread.currentThread());
: 将当前线程设置为锁的独占所有者。return true;
: 表示获取锁成功。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
: 初始状态,用于在 Worker
线程启动早期抑制中断。0
: 锁未被持有(解锁状态)。1
: 锁已被持有(锁定状态)。Worker
实现的 tryAcquire
逻辑非常简单,它不检查当前线程是否已经是锁的持有者,因此它是一个非可重入锁。如果一个线程已经持有了锁(状态为 1
),再次调用 lock()
会因为 compareAndSetState(0, 1)
失败而进入 AQS 的等待队列(或者 tryLock()
直接返回 false
)。Worker
使用 AQS 的独占模式。setExclusiveOwnerThread
和 getExclusiveOwnerThread
(间接通过 isHeldExclusively
判断) 用于此目的。compareAndSetState
实现无锁的原子状态更新,这是 AQS 的核心优势之一,用于高效地获取锁。-1
和 getState() >= 0
的检查,实现了对 Worker
线程启动初期中断的精细控制。runWorker
的循环中,每次执行任务前调用 w.lock()
,任务执行后在 finally
块中调用 w.unlock()
。这确保了任务执行期间,Worker
持有锁,防止了来自线程池管理操作(如 shutdownNow
)对正在执行任务的线程的意外中断。当 interruptIdleWorkers
尝试中断空闲 worker 时,它会先 w.tryLock()
,如果 worker 正在执行任务(已持有锁),tryLock
会失败,从而不会中断正在忙碌的 worker。通过这种方式,ThreadPoolExecutor.Worker
利用 AQS 的基本同步原语,以非常轻量级和高效的方式实现了一个满足其特定需求的非可重入锁和中断控制机制。
Worker
需要继承 AQSThreadPoolExecutor
中的 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
来实现互斥。但是,正如上面提到的,设计者有特定的需求:
ReentrantLock
是可重入的。如果需要严格的非可重入行为,以避免工作任务内部调用线程池方法时发生锁的重入,那么自定义一个非可重入锁是更直接的选择。runWorker
开始时通过 unlock()
(实际是将状态置为0)来解除这种抑制,这是一种精巧的控制方式。用 ReentrantLock
实现完全相同的启动时中断控制逻辑可能会稍微复杂一些,或者说不如直接操纵AQS的状态来得直接。Worker
这种内部辅助类,直接继承AQS实现一个极简的锁,可以避免引入 ReentrantLock
相对更重的对象(尽管现代JVM优化后差异可能不大,但在并发库的设计中,这种考量很常见)。总结来说,Worker
继承 AQS 是为了以一种轻量级且高度可控的方式实现一个非可重入锁,这个锁对于保护任务执行和精细控制 Worker
线程的中断状态至关重要。虽然可以使用 ReentrantLock
,但AQS提供了更大的灵活性来实现这些特定的语义。
execute(Runnable command)
这是向线程池提交任务的入口。其逻辑大致如下:
检查核心线程:
ctl
值,计算 workerCountOf(c)
。workerCount < corePoolSize
,尝试调用 addWorker(command, true)
创建一个新的核心线程来执行任务。如果成功,则返回。addWorker
失败 (可能因为并发修改 ctl
或线程工厂创建失败),重新获取 ctl
。尝试入队:
RUNNING
状态,并且 workQueue.offer(command)
成功(任务成功加入队列):
ctl
(recheck)。如果线程池不再是 RUNNING
状态 (例如,在入队操作期间被关闭),并且能成功从队列中移除该任务 (remove(command)
),则拒绝该任务 (reject(command)
)。workerCountOf(recheck) == 0
(可能所有线程都意外死掉了),则尝试启动一个新的非核心线程 (addWorker(null, false)
) 来处理队列中的任务 (但不携带新提交的 command
,因为 command
已经在队列里了)。尝试创建非核心线程:
addWorker(command, false)
创建一个新的非核心线程 (使用 maximumPoolSize
作为上限)。addWorker
成功,则返回。拒绝任务:
workerCount >= maximumPoolSize
且队列已满,或者线程池已关闭),则调用 reject(command)
执行拒绝策略。addWorker(Runnable firstTask, boolean core)
方法这个方法负责创建并启动一个新的 Worker
。
循环和CAS:使用一个 retry
标签和内部循环来处理并发。
RUNNING
状态,并且满足特定条件(如 STOP
状态,或 firstTask != null
,或队列为空),则不能添加 worker,返回 false
。workerCount
已经达到上限(corePoolSize
或 maximumPoolSize
,取决于 core
参数),返回 false
。ctl.compareAndSet(expect, expect + 1)
(即 compareAndIncrementWorkerCount
) 尝试原子地增加 workerCount
。如果成功,跳出 retry
循环。ctl
被其他线程修改了,重新读取 ctl
。如果状态改变,可能需要回到外层 retry
循环;否则,仅在内层循环重试 CAS。创建 Worker 和 Thread:
w = new Worker(firstTask)
:创建一个 Worker
对象。Worker
的构造函数会通过 threadFactory.newThread(this)
创建一个新线程,this
指的是 Worker
实例本身 (因为 Worker
实现了 Runnable
)。w.thread
。加锁并注册 Worker:
mainLock
。这是为了保护 workers
集合和 largestPoolSize【记录线程池生命周期内曾经达到的最大工作线程数】
等共享数据。firstTask
为 null
(不允许在关闭后添加空闲线程),则回滚。NEW
,抛异常。workers.add(w)
:将新 Worker
添加到 workers
集合中。largestPoolSize
。mainLock
。启动线程:
workerAdded
为 true
,则调用 container.start(t)
(在较新JDK中,通过 SharedThreadContainer
管理,旧版直接 t.start()
) 启动线程。workerStarted = true
。失败处理:
threadFactory
返回 null
,或启动线程时发生 OutOfMemoryError
),并且 workerStarted
为 false
,则调用 addWorkerFailed(w)
进行回滚操作(从 workers
移除,递减 workerCount
,尝试终止线程池)。返回 workerStarted
。
runWorker(Worker w)
方法这是工作线程的主循环,在 Worker.run()
中被调用。
wt
,获取 Worker w
的 firstTask
。w.firstTask = null;
(防止任务被重复执行)w.unlock();
// 允许中断,因为 Worker 初始化时 state 为 -1 (抑制中断)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 锁
}
getTask()
返回 null
时,表示 worker 需要退出。completedAbruptly
标记任务是否因异常退出循环。finally { processWorkerExit(w, completedAbruptly); }
getTask()
方法此方法负责从工作队列中获取任务,并处理 worker 的生命周期。
ctl
值。SHUTDOWN
,并且 (状态 >= STOP
或 workQueue.isEmpty()
),则 worker 必须退出。调用 decrementWorkerCount()
并返回 null
。workerCount
(wc
)。timed
):
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
wc > maximumPoolSize
(池动态缩小了) 或 (timed && timedOut
(上次poll超时了)))wc > 1
或 workQueue.isEmpty()
(如果这是最后一个线程且队列不空,则不能退出))compareAndDecrementWorkerCount(c)
,如果成功,返回 null
。否则继续循环。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
。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()
: 线程池完全终止后调用。这些方法默认是空实现,子类可以重写它们来添加自定义逻辑,如日志记录、性能统计、资源清理等。
1. 可以使用 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)))
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
的条件更新。保护复杂的数据结构:标准库中的很多集合类 (如 HashSet
, ArrayList
) 本身不是线程安全的。对它们的并发修改需要外部同步,通常通过锁来实现。
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 操作来实现更容易理解和维护。
深入分析一下 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)数据的访问。我们可以逐句解读:
Lock held on access to workers set and related bookkeeping.
mainLock
的首要职责是保护共享资源。
workers
:这是一个 HashSet
,存储了线程池中所有的工作线程。HashSet
本身不是线程安全的,并发地添加或删除元素会导致问题。related bookkeeping
:指的是像 largestPoolSize
(记录线程池曾经达到的最大线程数) 和 completedTaskCount
(已完成任务总数) 这样的统计数据。这些数据的更新也需要同步。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
的具体场景下,使用锁通常是“更可取”的。接下来的几点解释了为什么。Among the reasons is that this serializes interruptIdleWorkers, which avoids unnecessary interrupt storms, especially during shutdown.
interruptIdleWorkers
的作用:这个方法会遍历 workers
集合,并中断那些当前空闲(即正在等待任务)的工作线程。这通常在线程池关闭、配置更改(如缩减核心线程数)或需要唤醒线程来处理状态变化时调用。mainLock
进行序列化:
shutdown()
方法本身,或者因任务队列变空而触发的 tryTerminate()
)都尝试调用 interruptIdleWorkers
。workers
集合,并对同一批空闲线程发出重复的中断信号。mainLock
如何解决:通过在 interruptIdleWorkers
方法内部获取 mainLock
,确保了在任何时刻只有一个线程能够执行中断空闲线程的逻辑。这就将并发的“中断请求”变成了串行处理,避免了混乱和冗余。Otherwise exiting threads would concurrently interrupt those that have not yet interrupted.
mainLock
,多个即将退出的线程可能同时尝试中断其他尚未被中断的空闲线程。这不仅低效,而且如果中断逻辑与线程状态的判断之间存在微小的时间窗口,还可能导致一些难以追踪的并发问题。mainLock
确保了在检查和中断其他线程时,workers
集合的状态是稳定的,并且中断操作是有序的。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 操作来协调对并发集合和多个原子统计变量的修改会复杂得多,也更容易出错。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()
中的锁:这两个方法是线程池生命周期管理的关键。它们执行的操作包括:
runState
,通常通过对 ctl
的 CAS 操作完成)。checkShutdownAccess()
)。interruptIdleWorkers()
或 interruptWorkers()
)。workers
集合的稳定性至关重要:
checkShutdownAccess
):此方法会遍历 workers
集合,对每个 worker 的线程调用 SecurityManager.checkAccess()
。如果在遍历过程中 workers
集合被并发修改(添加或删除 worker),权限检查可能会基于一个不一致或过时的线程列表,导致检查不完整或不正确。workers
集合去中断线程时,如果集合可以并发修改,可能会导致 ConcurrentModificationException
(如果不是 HashSet
而是其他非并发集合),或者错过需要中断的线程,或者尝试中断已经被移除的线程。mainLock
确保了在执行这些关键的关闭步骤时(特别是权限检查和中断操作),workers
集合的内容是固定的、一致的。总结:
选择 ReentrantLock
而不是并发集合,是 ThreadPoolExecutor
设计者(Doug Lea)在权衡了正确性、代码简洁性、特定并发模式的处理(如避免中断风暴)以及复杂操作的原子性保证后做出的决定。
虽然并发集合在某些场景下能提供更好的吞吐量,但 mainLock
在这里提供了一种更粗粒度但更易于管理和推理的同步机制,有效地协调了对 workers
集合的访问、相关统计数据的更新以及关键的生命周期管理操作(如中断和关闭)。这种设计优先考虑了在复杂并发场景下的健壮性和可维护性。
原子性需求:首先分析操作需要达到的原子性级别。
临界区的大小和复杂度:
竞争的预期程度:
数据结构特性:操作的数据结构是否支持无锁操作,或者是否有现成的并发数据结构可用 (如 ConcurrentHashMap
, CopyOnWriteArrayList
)。如果使用非线程安全的结构,则必须加锁。
是否需要高级锁特性:如条件变量、公平性、可中断等。如果需要,ReentrantLock
是自然的选择。
性能测试和分析:在实际或模拟的负载下进行性能测试,分析瓶颈所在。不要过早优化,但要对关键路径的并发控制策略有清晰的认识。
要证明一段代码不能简单地用 CAS 替换,通常需要指出该代码段的原子性需求超出了单个 CAS 操作的能力范围:
涉及多个独立的内存位置的原子更新:
addWorker
中对 workers
集合和 largestPoolSize
的原子更新。这两个是不同的对象或字段,无法用一个 CAS 指令同时原子地修改。workers
集合的某个状态 并且 largestPoolSize
的某个值,然后原子地将它们更新为新状态和新值。CAS 通常操作的是一个固定大小的内存字。操作的逻辑依赖于一个非原子性的中间状态或计算:
需要保证一系列步骤的顺序性和不可分割性:
shutdown()
方法的步骤:advanceRunState()
(内部用 CAS) -> interruptIdleWorkers()
(遍历 workers
,需要锁) -> onShutdown()
。整个 shutdown()
的意图是作为一个整体的转换过程。虽然部分步骤可以是 CAS,但整个序列的协调和对 workers
的稳定访问需要 mainLock
。对复杂数据结构的内部修改:
HashSet
的内部节点进行 CAS 操作来添加或删除元素是非常复杂的,并且标准 HashSet
并非为此设计。你需要一个专门为此设计的并发数据结构。如果选择不使用这类并发数据结构,而是使用如 HashSet
这样的非线程安全集合,那么对其所有访问(读和写)都必须由同一个锁来保护,以确保一致性。总结 ThreadPoolExecutor
的实践:
ctl
(状态和工作线程数):设计为一个 AtomicInteger
,其更新逻辑(如增减 workerCount,改变 runState)被精心设计成可以通过 CAS 原子完成。这是因为状态和计数的改变可以被编码到单个整数中,并且操作相对直接。workers
(Worker 集合), largestPoolSize
, completedTaskCount
及相关操作 (如 shutdown
, interruptIdleWorkers
):这些涉及到对 HashSet
的修改、多个统计变量的更新、或者需要遍历 workers
集合并执行操作的复杂逻辑。这些操作的原子性和一致性通过 mainLock
来保证。这样做简化了设计,保证了正确性,并且如注释所说,避免了一些并发问题(如中断风暴)。在并发设计中,正确性通常是首要考虑的。在保证正确性的前提下,再根据具体场景和性能需求来选择合适的同步机制。CAS 是一种强大的工具,但并非万能药;锁在许多情况下仍然是保证复杂操作原子性的最直接和可靠的方式。