三分钟了解线程池的 Reentrantlock
三分钟了解线程池的 Reentrantlock
在 ThreadPoolExecutor 中,ReentrantLock(具体为 mainLock)是线程池实现线程安全的核心机制。其作用不仅限于简单的互斥访问,还涉及对线程池状态、工作线程集合(workers)以及关键统计数据的同步控制。
private final ReentrantLock mainLock = new ReentrantLock();
以下结合源码和实现原理进行深入分析:
一、ReentrantLock 的核心作用
1. 同步 workers 集合的操作
-
workers集合的非线程安全特性
workers是一个HashSet<Worker>,而HashSet本身是线程不安全的。通过mainLock确保对workers的增删操作(如addWorker和processWorkerExit)是原子的,避免并发修改导致的结构破坏(如ConcurrentModificationException)。 -
示例源码片段(
ThreadPoolExecutor类):private final HashSet<Worker> workers = new HashSet<>(); private boolean addWorker(Runnable firstTask, boolean core) { // ... 前置检查逻辑 mainLock.lock(); // 加锁 try { workers.add(w); // 原子性操作 int s = workers.size(); if (s > largestPoolSize) largestPoolSize = s; // 更新统计信息 } finally { mainLock.unlock(); // 解锁 } // ... 后续逻辑 }
2. 序列化中断操作(避免中断风暴)
-
问题背景
当调用interruptIdleWorkers()中断空闲线程时,若不加锁,多个线程可能同时触发中断,导致“中断风暴”(大量无意义的interrupt()调用)。例如,在shutdown()或动态调整线程池大小时,需要确保中断操作的原子性。 -
源码实现(
interruptIdleWorkers方法):private void interruptIdleWorkers(boolean onlyOne) { mainLock.lock(); try { for (Worker w : workers) { Thread t = w.thread; if (!t.isInterrupted() && w.tryLock()) { // Worker 内部锁 try { t.interrupt(); // 如果没有锁,多个线程同时触发导致“中断风暴” } catch (SecurityException ignore) { } finally { w.unlock(); } } if (onlyOne) break; } } finally { mainLock.unlock(); } }- 关键点:
mainLock确保遍历workers时集合不会被其他线程修改(如添加新 Worker)。Worker.tryLock()确保仅中断空闲线程(正在执行任务的线程会持有 Worker 自身的锁)。
- 关键点:
3. 维护线程池统计信息的一致性
- 统计指标:
largestPoolSize(历史最大线程数)、completedTaskCount(已完成任务数)等需要原子更新。 - 源码示例(
processWorkerExit方法):private void processWorkerExit(Worker w, boolean completedAbruptly) { mainLock.lock(); try { completedTaskCount += w.completedTasks; // 累加已完成任务数 workers.remove(w); // 移除 Worker } finally { mainLock.unlock(); } // ... 后续逻辑 }
二、ReentrantLock 的实现原理
1. 基于 AQS(AbstractQueuedSynchronizer)的锁机制
ReentrantLock 内部依赖 AQS 实现锁的获取与释放,核心是通过一个 volatile int state 变量和 CLH 队列(双向链表实现的等待队列)管理线程的竞争与排队。
-
关键组件:
state:表示锁的状态。state = 0:锁未被占用。state > 0:锁被占用,数值表示当前线程的重入次数。
- CLH 队列:存放等待锁的线程,确保公平性(若为公平锁)。
-
加锁流程(
lock()方法):- 通过 CAS(Compare-And-Swap)尝试将
state从 0 改为 1。 - 若成功,当前线程获取锁,记录持有锁的线程(
exclusiveOwnerThread)。 - 若失败(锁已被占用),将线程加入 CLH 队列并阻塞(通过
LockSupport.park())。
- 通过 CAS(Compare-And-Swap)尝试将
-
解锁流程(
unlock()方法):- 减少
state值(若state减到 0,表示完全释放锁)。 - 唤醒 CLH 队列中的下一个等待线程。
- 减少
2. 可重入性的实现
当线程已持有锁时,再次调用 lock() 会直接增加 state 的值(重入次数),而不是阻塞。释放锁时需调用 unlock() 与重入次数匹配的次数,才能彻底释放锁。
- 源码片段(
ReentrantLock.Sync类):final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { // 已持有锁的线程 setState(c + acquires); // 增加重入次数 return true; } return false; }
三、为什么选择 ReentrantLock 而非其他同步机制?
1. 对比 synchronized 关键字
-
灵活性:
ReentrantLock支持非阻塞尝试获取锁(tryLock())、可中断锁(lockInterruptibly())、超时锁(tryLock(timeout)),而synchronized不支持。
(注:ThreadPoolExecutor未使用这些高级特性,但保留了扩展可能性。) -
性能:
在 JDK 6 之后,synchronized经过优化(偏向锁、轻量级锁),性能与ReentrantLock接近,但ReentrantLock在高竞争场景下仍有一定优势。
2. 对比并发集合(如 ConcurrentHashMap)
源码注释中明确提到:“虽然可以使用某种并发集合,但使用锁通常是更好的选择”。原因包括:
- 操作原子性:例如
interruptIdleWorkers()需要遍历并修改workers,若用并发集合,遍历时可能需要额外同步。 - 避免中断风暴:锁可以序列化中断操作,防止多个线程同时触发中断。
- 简化统计逻辑:通过锁保护多个相关操作(如更新
workers和largestPoolSize)。
四、ReentrantLock 在关闭线程池时的关键作用
在 shutdown() 和 shutdownNow() 中,mainLock 确保关闭过程的原子性:
- 获取锁:防止其他线程修改
workers。 - 遍历并中断所有线程:
public void shutdown() { final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { checkShutdownAccess(); advanceRunState(SHUTDOWN); interruptIdleWorkers(); // 需要锁保护 onShutdown(); } finally { mainLock.unlock(); } tryTerminate(); } - 释放锁:确保后续操作可以继续修改线程池状态。
五、总结
- 线程安全:
mainLock保护workers集合和关键状态变量,确保原子操作。 - 避免竞态条件:通过锁序列化中断和状态更新操作。
- 可重入性:允许同一线程在持有锁时重入(如嵌套调用需要锁的方法)。
- 性能与设计权衡:粗粒度锁简化了同步逻辑,虽可能在高并发下成为瓶颈,但符合线程池设计目标(任务执行是主要性能开销点)。
通过 ReentrantLock 的精细控制,ThreadPoolExecutor 在多线程环境中高效且安全地管理了工作线程的生命周期和任务执行。
六、高频面试题
这些问题覆盖了锁的选择、线程安全机制、AQS 原理及实际应用场景,深入理解这些内容不仅能回答面试问题,还能帮助优化实际高并发场景下的线程池设计。
1. 为什么 ThreadPoolExecutor 使用 ReentrantLock(mainLock)而不是 synchronized 关键字?
- 核心答案:
ReentrantLock提供了更灵活的锁控制机制,例如:- 可中断的锁获取(
lockInterruptibly()):允许在等待锁时响应中断。 - 超时尝试获取锁(
tryLock(timeout)):避免无限阻塞。 - 公平性选择:支持公平锁与非公平锁(默认非公平锁,性能更高)。
尽管ThreadPoolExecutor未直接使用这些高级功能,但ReentrantLock的代码结构更清晰,且能更好地控制锁的粒度(如仅在操作workers集合时加锁)。此外,源码注释提到,使用ReentrantLock可以序列化中断操作(如interruptIdleWorkers),避免多线程并发中断导致的中断风暴。
- 可中断的锁获取(
2. 解释 mainLock 如何避免“中断风暴”(interrupt storms)?
- 核心答案:
interruptIdleWorkers方法通过mainLock保证同一时间只有一个线程能遍历workers集合并中断空闲线程。若不加锁,多个线程可能同时调用interrupt(),导致以下问题:- 冗余中断:同一线程被多次中断。
- 状态不一致:在遍历
workers时集合被其他线程修改(如新增Worker)。
源码实现:
private void interruptIdleWorkers(boolean onlyOne) { mainLock.lock(); // 加锁确保串行化操作 try { for (Worker w : workers) { // 仅中断空闲线程(通过 Worker 的 tryLock 判断) if (w.tryLock()) { t.interrupt(); w.unlock(); } } } finally { mainLock.unlock(); } }
3. ReentrantLock 的可重入性在 ThreadPoolExecutor 中的实际应用场景是什么?
- 核心答案:
可重入性允许同一线程多次获取同一把锁,避免死锁。例如:- 嵌套调用:在
shutdown()方法中,主线程先获取mainLock,随后调用interruptIdleWorkers(),后者也需要获取mainLock。由于锁可重入,线程不会阻塞自身。 - 递归操作:若某个方法内部递归调用另一个需要锁的方法,可重入性确保线程安全。
- 嵌套调用:在
4. 为什么 workers 集合使用 HashSet 而非并发集合(如 ConcurrentHashMap)?
- 核心答案:
使用HashSet加mainLock的原因包括:- 原子性复合操作:例如
addWorker中需要同时添加Worker和更新largestPoolSize,使用锁可以保证这两个操作的原子性。 - 避免遍历时的并发修改:并发集合(如
ConcurrentHashMap)的迭代器是弱一致性的,而interruptIdleWorkers需要严格遍历当前所有Worker。 - 简化统计逻辑:锁可以统一保护多个相关变量(如
workers、completedTaskCount等)。
- 原子性复合操作:例如
5. 在 addWorker 和 processWorkerExit 方法中,mainLock 的作用是什么?
- 核心答案:
addWorker:- 保护
workers集合的添加操作。 - 更新
largestPoolSize(历史最大线程数)。
- 保护
processWorkerExit:- 保护
workers集合的移除操作。 - 累加
completedTaskCount(已完成任务数)。
源码片段:
- 保护
// addWorker 中的加锁逻辑 mainLock.lock(); try { workers.add(w); int s = workers.size(); if (s > largestPoolSize) largestPoolSize = s; } finally { mainLock.unlock(); }
6. ReentrantLock 如何基于 AQS(AbstractQueuedSynchronizer)实现锁的获取与释放?
- 核心答案:
ReentrantLock内部通过 AQS 的state变量和 CLH 队列管理锁状态:- 加锁流程:
- 通过 CAS 尝试将
state从 0 改为 1(获取锁)。 - 若失败,将线程加入 CLH 队列并阻塞(
LockSupport.park())。
- 通过 CAS 尝试将
- 释放流程:
- 减少
state的值。 - 若
state变为 0,唤醒队列中的下一个线程。
- 减少
- 可重入性:
线程重复获取锁时,state递增;释放时需对应次数递减。
- 加锁流程:
7. 为什么在 shutdown() 和 shutdownNow() 方法中必须持有 mainLock?
- 核心答案:
关闭线程池时需保证:workers集合的稳定性:防止在遍历中断线程时,其他线程修改workers(如新增Worker)。- 原子性状态切换:将线程池状态改为
SHUTDOWN或STOP,并确保后续操作(如中断线程)基于最新状态。
源码示例:
public void shutdown() { mainLock.lock(); try { advanceRunState(SHUTDOWN); // 原子性更新状态 interruptIdleWorkers(); // 需要锁保护遍历操作 } finally { mainLock.unlock(); } }
8. Worker 类的内部锁与 mainLock 的区别是什么?各自的作用是什么?
- 核心答案:
Worker的内部锁:- 继承自
AQS,实现为不可重入锁。 - 作用:保护任务执行过程。任务运行时,
Worker会加锁,防止任务被并发执行或错误中断。
- 继承自
mainLock:- 保护线程池的全局状态(如
workers集合、largestPoolSize)。
- 保护线程池的全局状态(如
- 区别:
Worker的锁是细粒度的(单任务),mainLock是粗粒度的(全局状态)。
9. ReentrantLock 的公平模式与非公平模式在 ThreadPoolExecutor 中是如何选择的?
- 核心答案:
ThreadPoolExecutor默认使用 非公平锁(new ReentrantLock()默认非公平),原因包括:- 性能优势:非公平锁减少线程切换开销,允许新请求“插队”直接获取锁(若锁恰好释放)。
- 避免饥饿问题:线程池场景中,任务提交是动态的,非公平锁能更快响应新请求。
10. 如果移除 mainLock,直接使用 synchronized 修饰方法,会引发什么问题?
- 核心答案:
- 锁粒度过粗:
synchronized方法锁住整个对象实例,导致无关操作(如getPoolSize())也被阻塞,降低并发性能。 - 无法灵活控制锁:例如无法实现
tryLock(),可能因锁竞争导致任务提交延迟。 - 中断风暴风险:
synchronized无法序列化interruptIdleWorkers操作,可能引发并发中断。
- 锁粒度过粗:
11. 为什么 interruptIdleWorkers 方法在遍历 workers 时,需要同时持有 mainLock 和 Worker 的内部锁?
- 核心答案:
-
mainLock的作用- 保护
workers集合的稳定性:
在遍历workers时,需要确保集合不被其他线程修改(如addWorker或processWorkerExit)。若没有mainLock,遍历过程中可能有线程被动态添加或移除,导致ConcurrentModificationException或漏掉部分Worker。 - 序列化中断操作:
防止多个线程并发调用interruptIdleWorkers,导致同一线程被重复中断(中断风暴)。
- 保护
-
Worker内部锁的作用- 判断线程是否空闲:
Worker继承自AQS,其内部锁用于标识工作线程是否正在执行任务。- 若
Worker.tryLock()成功,表示线程处于空闲状态(未执行任务),可以安全中断。 - 若失败,表示线程正在执行任务,此时中断可能导致任务执行异常(需避免)。
- 若
- 判断线程是否空闲:
-
协同机制
mainLock保证遍历的原子性,确保遍历期间workers集合不变。Worker锁保证中断的安全性,仅中断空闲线程,避免干扰正在执行的任务。
源码分析
private void interruptIdleWorkers(boolean onlyOne) {
mainLock.lock(); // 保护 workers 集合的遍历
try {
for (Worker w : workers) {
Thread t = w.thread;
// 通过 Worker 的 tryLock() 判断是否空闲
if (!t.isInterrupted() && w.tryLock()) {
try {
t.interrupt(); // 仅中断空闲线程
} catch (SecurityException ignore) {
} finally {
w.unlock();
}
}
if (onlyOne) break;
}
} finally {
mainLock.unlock();
}
}
- 关键点:
- 持有
mainLock确保遍历workers时集合稳定。 - 通过
Worker.tryLock()判断线程是否空闲,避免中断正在执行任务的线程。
- 持有
场景示例
假设没有 Worker 的内部锁:
- 线程 A 正在执行任务(未释放
Worker锁),此时interruptIdleWorkers遍历到该Worker。 - 直接调用
t.interrupt()会中断正在执行的任务,可能导致数据不一致或任务异常终止。
12. 如何通过 tryLock() 优化 ThreadPoolExecutor 的锁竞争?
- 核心答案:
ReentrantLock的tryLock()允许线程 非阻塞尝试获取锁,若锁不可用则立即返回失败,避免线程阻塞。通过以下方式优化锁竞争:
-
减少锁的持有时间
在需要锁的代码块中,优先尝试获取锁,若失败则执行其他非临界区操作(如重试或记录日志),避免长时间阻塞。 -
避免死锁
若锁的获取顺序可能引发死锁(如多个线程交叉请求锁),tryLock()可以主动放弃锁请求并重试。 -
动态调整锁粒度
将粗粒度锁拆分为多个细粒度锁,结合tryLock()实现更灵活的并发控制。
优化示例
假设需要修改 ThreadPoolExecutor 的统计信息(如 largestPoolSize),可以尝试以下优化:
场景:更新 largestPoolSize
private void updateLargestPoolSize() {
ReentrantLock lock = this.mainLock;
boolean acquired = lock.tryLock(); // 非阻塞尝试获取锁
if (acquired) {
try {
int size = workers.size();
if (size > largestPoolSize) {
largestPoolSize = size;
}
} finally {
lock.unlock();
}
} else {
// 锁被占用时,跳过更新(最终一致性)或延迟重试
}
}
- 优点:
- 若
mainLock未被占用,快速更新统计信息。 - 若锁被占用,避免阻塞当前线程(如任务提交线程),减少对主流程的影响。
- 若
场景:避免中断风暴
在 interruptIdleWorkers 中,若锁竞争激烈,可以结合 tryLock() 限制重试次数:
private void interruptIdleWorkers() {
boolean locked = mainLock.tryLock(10, TimeUnit.MILLISECONDS); // 限时等待
if (locked) {
try {
for (Worker w : workers) {
// ... 中断逻辑
}
} finally {
mainLock.unlock();
}
} else {
// 记录竞争失败,或延迟重试
}
}
- 优点:
- 防止线程因锁竞争长时间阻塞,影响线程池吞吐量。
注意事项
- 权衡一致性:
tryLock()可能导致某些操作被跳过(如统计更新),需确保业务允许最终一致性。 - 避免活锁:
若使用tryLock()配合重试机制,需设置最大重试次数或退避策略,避免活锁。

浙公网安备 33010602011771号