深入剖析ReentrantLock底层原理:从AQS到公平锁的源码级解析
📖 摘要
关键词:
ReentrantLock原理
、AQS机制
、公平锁
、条件变量
、可重入锁
本文深入解析Java并发包中的ReentrantLock
实现,涵盖AQS核心机制、公平/非公平锁差异、Condition条件变量等核心内容,结合源码与流程图,彻底揭示其高并发设计的精妙之处!
📑 目录
- ReentrantLock核心特性速览
- AQS(AbstractQueuedSynchronizer)架构解析
- ReentrantLock的加锁/解锁流程
- 公平锁与非公平锁的底层差异
- Condition条件变量的实现原理
- ReentrantLock vs synchronized深度对比
- 性能优化与避坑指南
- 高频面试题与总结
1. ReentrantLock核心特性速览
作为java.util.concurrent
包的明星类,ReentrantLock
相比synchronized
具备以下特性:
- ✅ 可中断锁:
lockInterruptibly()
支持响应中断。 - ✅ 公平性选择:构造函数指定公平/非公平策略。
- ✅ 多条件变量:通过
newCondition()
创建多个等待队列。 - ✅ 尝试获取锁:
tryLock()
支持超时或立即返回。 - ✅ 可重入性:同一线程可重复获取锁。
2. AQS(AbstractQueuedSynchronizer)架构解析
ReentrantLock
的底层依赖AQS框架,其核心组成如下:
2.1 AQS数据结构
public abstract class AbstractQueuedSynchronizer {
// 同步状态(锁的重入次数)
private volatile int state;
// CLH队列节点(双向链表)
static final class Node {
volatile int waitStatus; // 等待状态(CANCELLED、SIGNAL等)
volatile Node prev;
volatile Node next;
volatile Thread thread; // 等待线程
Node nextWaiter; // 条件队列专用
}
// 队列头尾指针
private transient volatile Node head;
private transient volatile Node tail;
}
2.2 AQS的核心设计思想
- 模板方法模式:子类(如ReentrantLock.Sync)实现
tryAcquire
/tryRelease
等钩子方法。 - CLH队列:Craig, Landin, and Hagersten(CLH)锁队列的变种,用于管理竞争线程。
- 状态管理:
state
字段表示锁的持有次数(可重入性)。
3. ReentrantLock的加锁/解锁流程
3.1 加锁流程(以非公平锁为例)
1. 线程调用lock()方法
2. 直接尝试CAS修改state(快速路径):
- 成功:设置当前线程为独占所有者(exclusiveOwnerThread)
- 失败:进入acquire()方法
3. acquire()调用tryAcquire()再次尝试获取锁
4. 若仍失败,将线程封装为Node加入CLH队列
5. 进入自旋,不断检查前驱节点是否为头节点并尝试获取锁
6. 若获取成功,将当前节点设为头节点;否则挂起线程(LockSupport.park())
源码关键路径:
final void lock() {
if (compareAndSetState(0, 1)) // 非公平锁直接抢
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
3.2 解锁流程
1. 线程调用unlock()方法
2. 调用tryRelease()减少state值
3. 若state减至0,清空独占所有者线程
4. 唤醒CLH队列中的下一个线程(LockSupport.unpark())
源码关键路径:
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 唤醒后继节点
return true;
}
return false;
}
4. 公平锁与非公平锁的底层差异
4.1 非公平锁(默认策略)
- 加锁特点:线程可直接插队尝试获取锁(即使队列中有等待线程)。
- 优势:减少线程切换,吞吐量高。
- 源码差异:
final boolean nonfairTryAcquire(int acquires) { // 直接尝试CAS,不检查队列 }
4.2 公平锁
- 加锁特点:必须按CLH队列顺序获取锁。
- 优势:避免线程饥饿。
- 源码差异:
protected final boolean tryAcquire(int acquires) { if (!hasQueuedPredecessors()) { // 检查是否有前驱节点 // 执行CAS } // ... }
4.3 性能对比场景
场景 | 非公平锁 | 公平锁 |
---|---|---|
高竞争短任务 | 更优 | 较差 |
低竞争长任务 | 接近 | 接近 |
严格顺序需求 | 不适用 | 必须 |
5. Condition条件变量的实现原理
5.1 Condition与Object监视器方法的对比
功能 | Condition | Object 监视器 |
---|---|---|
等待队列数量 | 多个 | 单个 |
唤醒精确性 | 可指定唤醒某个队列 | 只能随机或全部唤醒 |
超时等待 | 支持awaitNanos() | 不支持 |
5.2 Condition内部实现
- 等待队列:每个Condition对象维护一个单向链表队列。
- 节点转移:
await()
:将节点从CLH锁队列转移到Condition等待队列。signal()
:将节点从Condition队列移回CLH锁队列。
await()流程:
1. 创建Condition节点加入条件队列
2. 完全释放锁(state=0)
3. 进入循环,检查是否在同步队列:
- 若未在,挂起线程
4. 被唤醒后,尝试重新获取锁
signal()流程:
1. 将条件队列的头节点转移到CLH锁队列
2. 修改节点状态为可竞争(SIGNAL)
6. ReentrantLock vs synchronized深度对比
维度 | ReentrantLock | synchronized |
---|---|---|
锁实现 | 基于AQS(Java代码) | JVM内置(C++实现) |
锁释放 | 必须手动unlock() | 自动释放 |
公平性 | 支持配置 | 非公平 |
条件变量 | 多Condition | 单等待队列 |
锁中断 | 支持lockInterruptibly() | 不支持 |
性能 | 高竞争下更优 | JDK6后优化接近 |
锁绑定 | 灵活(可跨方法) | 方法或代码块 |
7. 性能优化与避坑指南
7.1 正确使用姿势
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock(); // 必须放在finally块!
}
7.2 常见问题与解决方案
- 死锁:按固定顺序获取多个锁。
- 锁泄漏:确保所有路径都调用
unlock()
。 - 过度竞争:缩小锁粒度或使用读写锁(ReentrantReadWriteLock)。
7.3 监控工具
- Jstack:查看线程锁持有情况。
- Arthas:实时监控锁竞争状态。
8. 高频面试题与总结
高频面试题:
-
Q:AQS为什么用双向链表?
A:便于处理取消节点(CANCELLED状态),快速移除失效节点。 -
Q:ReentrantLock如何实现可重入?
A:通过state
计数器记录重入次数,释放时递减至0才完全释放。 -
Q:非公平锁是否会导致饥饿?
A:理论上可能,但实际高吞吐场景中概率极低。
核心总结:
- AQS是核心:理解CLH队列与state状态流转。
- 公平性选择:根据场景权衡吞吐量与公平性。
- 条件变量:复杂等待/通知场景的首选工具。