CLH 队列如何在 ReentrantReadWriteLock 中被扩展?
这是一个非常深入、底层的问题,下面我们将系统性剖析:CLH 队列 是如何被扩展和复用,在 Java 并发包中的 ReentrantReadWriteLock(读写锁)中实现多线程的安全调度。
一、前提背景:CLH 是 AQS 的基础队列模型
ReentrantReadWriteLock是通过AQS实现的。AQS本身使用**变种的 CLH 队列(双向 FIFO 队列)**来实现线程阻塞与唤醒。ReadWriteLock的挑战在于它要处理两种资源访问方式:- 多线程 共享读(Shared)
- 单线程 独占写(Exclusive)
这就意味着:一套队列要支持两种模式的线程调度行为。
二、核心类关系结构
ReentrantReadWriteLock
├── Sync(继承 AQS)
├── FairSync
└── NonfairSync
ReentrantReadWriteLock.Sync是继承自AbstractQueuedSynchronizer的。- 它定义了
tryAcquire,tryRelease,tryAcquireShared,tryReleaseShared等方法,支持独占与共享两种模式。
三、CLH 队列如何扩展支持读写锁
1. Node 节点中的共享/独占标识
AQS 的 Node 类中有两个标识模式的常量:
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
在构建 CLH 队列时:
- 读线程(共享)→
node.shared = SHARED - 写线程(独占)→
node.shared = EXCLUSIVE
CLH 队列结构变成如下:
head → [W] → [R] → [R] → [W] → ...
其中 [W] 表示写节点(Exclusive),[R] 表示读节点(Shared)
2. tryAcquireShared() 与 tryAcquire() 区别
- 写锁使用
AQS.tryAcquire()(独占模式) - 读锁使用
AQS.tryAcquireShared()(共享模式)
示例:读线程调用 lock() → 尝试共享获取
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
// 判断是否已有写锁
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current) {
return -1;
}
// 尝试增加读锁数量(state 低 16 位)
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
if (compareAndSetState(c, c + SHARED_UNIT)) {
...
return 1; // 成功
}
return -1; // 失败进入 CLH 队列
}
说明:
- 读线程失败后,会以共享模式加入 CLH 队列。
- 唤醒时,只要前面没有写线程,就可以同时唤醒多个读线程。
四、扩展关键点:读写队列混合调度策略
1. 读共享(Read-Shared)策略
- 多个读线程可同时持有锁,前提是没有写线程持有或在前面排队。
- 条件判断:当前队列中前面是否有写线程。
如果队首是写线程,读线程将被阻塞。
2. 写独占(Write-Exclusive)策略
- 写线程必须等所有读线程和其他写线程释放后,才可获取锁。
- 写线程获取成功后,其他线程都必须排队。
五、CLH 在读写锁中的实际表现形式
CLH 队列混合状态:
head → [W] → [R] → [R] → [W] → [R] → ...
↑ ↑ ↑
这些读线程不能执行(被前面的写锁阻塞)
当写锁释放后:
- 队列从前向后检查是否可以批量唤醒多个共享节点(读线程)。
六、为什么用 CLH 扩展而不是另建队列?
优点:
| 优点 | 说明 |
|---|---|
| 复用统一队列 | 不需要维护两个独立的读/写等待队列,降低复杂度 |
| 同步/共享通用调度机制 | AQS 支持共享 & 独占,队列节点设计已能区分 |
| 保持 FIFO 公平策略 | 写线程也能基于 FIFO 排队,避免写饥饿 |
| 支持条件队列(Condition) | 保留 ConditionObject 实现机制,无需重写 Condition 管理逻辑 |
七、源码入口建议阅读路径
| 功能 | 源码方法 |
|---|---|
| 写锁加锁 | ReentrantReadWriteLock.WriteLock.lock() |
| 写锁核心逻辑 | Sync.tryAcquire() |
| 读锁加锁 | ReentrantReadWriteLock.ReadLock.lock() |
| 读锁核心逻辑 | Sync.tryAcquireShared() |
| CLH 入队逻辑 | AQS.enqueue(Node) |
| 节点模式判断 | Node.shared, Node.exclusive |
| 唤醒读线程逻辑 | doReleaseShared() |
八、参考资料
- OpenJDK ReentrantReadWriteLock 源码
- OpenJDK AbstractQueuedSynchronizer 源码
- Java 并发编程实战 第 13 章、14 章
- 深入理解 Java 虚拟机 — 锁机制实现
总结口诀
CLH 队列配 AQS,读写模式能共舞;
独占抢锁是写线程,共享排队是读线程;
一个队列搞定它,公平高效没话说!
更多详细内容请关注其他相关文章!