AQS 深度剖析:ReentrantLock 公平锁如何通过 CLH 队列保证顺序
一、ReentrantLock 核心概念
1.1 ReentrantLock 概述
ReentrantLock 是 Java 并发包中基于 AQS(AbstractQueuedSynchronizer)框架实现的可重入互斥锁,支持公平锁与非公平锁两种模式。
可重入性:同一线程可以多次获取同一把锁,通过计数器实现,解锁需要相同次数的释放。
1.2 与 Synchronized 的对比
| 实现机制 | 基于 AQS 框架 | 基于 JVM 监视器模式 |
| 灵活性 | 支持中断、超时、尝试获取锁 | 不灵活,只能阻塞等待 |
| 锁释放 | 必须显式调用 unlock() | 自动释放监视器 |
| 锁策略 | 支持公平锁与非公平锁 | 仅非公平锁 |
| 条件队列 | 可关联多个 Condition 队列 | 单个等待队列 |
| 性能特点 | 高竞争下性能更好 | 低竞争下性能更优 |
| 可重入性 | 支持可重入 | 支持可重入 |
二、AQS 核心架构与 CLH 队列
2.1 AQS 设计哲学
AQS 采用模板方法模式,提供构建锁和同步器的框架,核心思想是:
- 通过一个 volatile 的 state 表示同步状态
- 使用 CLH 变体队列管理等待线程
- 提供 acquire/release 模板方法
2.2 CLH 队列变体
AQS 中的 CLH 队列是对原始 CLH 锁的改进:
// AQS 内部节点类
static final class Node {
// 等待状态
static final int CANCELLED = 1; // 节点已取消
static final int SIGNAL = –1; // 后继节点需要唤醒
static final int CONDITION = –2; // 在条件队列中等待
static final int PROPAGATE = –3; // 共享模式下传播
volatile int waitStatus; // 节点状态
volatile Node prev; // 前驱节点
volatile Node next; // 后继节点
volatile Thread thread; // 等待线程
Node nextWaiter; // 共享模式或条件队列链接
}
CLH 队列特点:
- 双向链表:便于节点的移除和遍历
- 虚拟头节点:简化边界条件处理
- 状态标识:支持多种等待状态
三、公平锁顺序保证机制
3.1 公平锁的核心逻辑
公平锁的核心在于 “先来先服务” 原则,通过以下两点保证:
// 公平锁实现
static final class FairSync extends Sync {
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 锁未被占用
if (c == 0) {
// 关键:检查是否有前驱节点
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 重入逻辑
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
3.2 hasQueuedPredecessors() 方法详解
这是公平锁顺序保证的核心方法:
public final boolean hasQueuedPredecessors() {
Node t = tail; // 尾节点
Node h = head; // 头节点
Node s;
// 返回 true 表示有前驱节点,应该排队等待
return h != t && // 队列不为空
((s = h.next) == null || // 头节点的后继为空(并发初始化情况)
s.thread != Thread.currentThread()); // 后继节点的线程不是当前线程
}
方法逻辑分析:
3.3 完整获取锁流程
// 公平锁的 lock() 方法调用链
public void lock() {
sync.lock(); // 调用 FairSync.lock()
}
// FairSync.lock() -> AQS.acquire(1)
public final void acquire(int arg) {
if (!tryAcquire(arg) && // 尝试获取锁
acquireQueued( // 获取失败,进入队列
addWaiter(Node.EXCLUSIVE), arg)) // 创建节点并入队
selfInterrupt(); // 恢复中断状态
}
步骤 1:尝试获取锁
通过 tryAcquire() 检查:
- 锁是否空闲
- 是否有前驱节点在等待
步骤 2:创建节点并入队
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
// 快速路径:尝试直接添加到队尾
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 慢速路径:队列为空或 CAS 失败
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) { // 自旋直到成功
Node t = tail;
if (t == null) { // 队列为空,初始化
if (compareAndSetHead(new Node())) // 创建虚拟头节点
tail = head; // 头尾指向同一个虚拟节点
} else {
node.prev = t;
if (compareAndSetTail(t, node)) { // CAS 设置尾节点
t.next = node;
return t;
}
}
}
}
步骤 3:队列中等待
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) { // 自旋等待
final Node p = node.predecessor(); // 获取前驱节点
// 关键:只有前驱是头节点时才尝试获取锁
if (p == head && tryAcquire(arg)) {
setHead(node); // 设置当前节点为头节点
p.next = null; // 断开旧头节点,帮助 GC
failed = false;
return interrupted;
}
// 检查是否需要阻塞
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node); // 取消获取
}
}
shouldParkAfterFailedAcquire() 方法:
- 检查并更新前驱节点的状态
- 确保前驱节点的状态为 SIGNAL,表示释放锁时会唤醒后继节点
3.4 锁释放流程
// unlock() 调用链
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;
}
unparkSuccessor() 唤醒后继
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0); // 清除状态
Node s = node.next; // 后继节点
// 后继节点为空或已取消
if (s == null || s.waitStatus > 0) {
s = null;
// 从尾向前遍历,找到最靠近头部的有效节点
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread); // 唤醒线程
}
四、队列状态变化示例
4.1 初始状态
head → [dummy] ← tail
waitStatus=0
4.2 线程 A 入队
head → [dummy] ↔ [Thread-A] ← tail
↑
prev/next 链接
4.3 线程 B 入队
head → [dummy] ↔ [Thread-A] ↔ [Thread-B] ← tail
4.4 线程 A 获取锁
head → [Thread-A] ↔ [Thread-B] ← tail
(原 dummy 节点被断开)
4.5 线程 A 释放锁,唤醒 B
head → [Thread-B] ← tail
五、关键设计要点分析
5.1 顺序保证机制
| 入队顺序 | 新线程总是插入队尾 | 保证到达顺序 |
| 出队顺序 | 只有头节点的后继能尝试获取锁 | 保证 FIFO 顺序 |
| 严格检查 | hasQueuedPredecessors() 检查 | 防止插队 |
| 有序唤醒 | 只唤醒第一个有效等待节点 | 避免争抢 |
5.2 避免饥饿的设计
// 公平锁防止饥饿的核心代码
if (c == 0) {
// 即使锁空闲,也要检查是否有等待者
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
// 只有无等待者才能获取
}
}
设计哲学:公平锁牺牲了部分吞吐量来保证绝对的公平性,避免了线程饥饿问题。
5.3 双向队列的优势
| 高效取消 | 从任意位置移除节点都高效 |
| 反向遍历 | 处理取消节点时可从后向前遍历 |
| 条件队列 | 便于与条件队列集成 |
5.4 虚拟头节点设计
// 初始化队列时创建虚拟节点
if (t == null) { // 队列为空
if (compareAndSetHead(new Node())) // 创建虚拟节点
tail = head;
}
虚拟头节点作用:
- 简化边界条件处理
- 避免空指针异常
- 提供稳定的同步状态
六、公平锁与非公平锁对比
6.1 实现差异
// 非公平锁实现
static final class NonfairSync extends Sync {
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 直接尝试 CAS,不检查队列!
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 重入逻辑相同…
}
6.2 性能对比
| 低竞争 | 性能稍差 | 性能更好 |
| 高竞争 | 吞吐量较低 | 吞吐量更高 |
| 响应时间 | 更可预测 | 可能波动较大 |
| 饥饿风险 | 无 | 可能存在 |
6.3 适用场景
使用公平锁的场景:
- 需要严格的先来先服务顺序
- 线程等待时间差异较大
- 防止线程饥饿是首要考虑
使用非公平锁的场景:
- 追求更高的吞吐量
- 线程等待时间较短
- 能够容忍一定的顺序不公平
七、常见问题解答
Q1:为什么 CLH 队列使用双向链表?
A:双向链表的优势:
Q2:公平锁如何避免死锁?
A:公平锁本身不会引入死锁,死锁通常由以下原因引起:
// 避免死锁的建议
ReentrantLock lock1 = new ReentrantLock(true);
ReentrantLock lock2 = new ReentrantLock(true);
// 统一获取顺序
public void safeMethod() {
lock1.lock();
try {
lock2.lock();
try {
// 业务逻辑
} finally {
lock2.unlock();
}
} finally {
lock1.unlock();
}
}
Q3:公平锁的性能瓶颈是什么?
A:主要瓶颈包括:
Q4:如何选择公平锁与非公平锁?
决策矩阵:
| 顺序要求 | 高 ✓ | 低 |
| 吞吐量需求 | 低 | 高 ✓ |
| 防止饥饿 | 必须 ✓ | 不重要 |
| 实现复杂度 | 较高 | 较低 ✓ |
八、最佳实践与性能优化
8.1 使用模式
public class FairLockExample {
private final ReentrantLock fairLock = new ReentrantLock(true);
public void criticalSection() {
fairLock.lock(); // 获取公平锁
try {
// 访问共享资源
performOperation();
} finally {
fairLock.unlock(); // 必须释放锁
}
}
// 带超时的获取
public boolean tryCriticalSection(long timeout, TimeUnit unit)
throws InterruptedException {
if (fairLock.tryLock(timeout, unit)) {
try {
performOperation();
return true;
} finally {
fairLock.unlock();
}
}
return false;
}
}
8.2 监控与调试
// 监控队列长度
public void monitorQueue() {
ReentrantLock lock = new ReentrantLock(true);
// 获取等待队列长度
int queueLength = lock.getQueueLength();
System.out.println("等待线程数: " + queueLength);
// 检查是否有线程在等待
boolean hasQueuedThreads = lock.hasQueuedThreads();
// 获取等待线程
Collection<Thread> queuedThreads = lock.getQueuedThreads();
}
8.3 性能调优建议
九、源码级别调试技巧
9.1 关键断点设置
// 调试断点位置:
// 1. FairSync.tryAcquire() – 公平获取逻辑
// 2. AbstractQueuedSynchronizer.hasQueuedPredecessors() – 队列检查
// 3. AbstractQueuedSynchronizer.addWaiter() – 入队逻辑
// 4. AbstractQueuedSynchronizer.unparkSuccessor() – 唤醒逻辑
9.2 线程转储分析
jstack <pid> > thread_dump.txt
// 查找锁信息:
// "locked <0x000000076ad27fc0> (a java.util.concurrent.locks.ReentrantLock$FairSync)"
// "waiting on <0x000000076ad27fc0> (a java.util.concurrent.locks.ReentrantLock$FairSync)"
十、总结
ReentrantLock 公平锁通过 AQS 的 CLH 队列变种实现了严格的 FIFO 顺序保证,其核心机制包括:
公平锁的设计体现了 “公平性优先” 的原则,虽然可能降低吞吐量,但确保了:
- 绝对的先来先服务顺序
- 无线程饥饿问题
- 可预测的等待时间
在实际应用中,应根据具体场景选择锁策略:
- 公平锁:对顺序敏感、防止饥饿的场景
- 非公平锁:追求高吞吐、低延迟的场景
理解 AQS 和 CLH 队列的实现原理,不仅有助于更好地使用 ReentrantLock,也为理解 Java 并发框架的其他组件(如 CountDownLatch、Semaphore 等)奠定了基础。



