CLH 队列如何在 ReentrantReadWriteLock 中被扩展?
                           
天天向上
发布: 2025-07-20 11:00:20

原创
742 人浏览过

这是一个非常深入、底层的问题,下面我们将系统性剖析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()

八、参考资料


总结口诀

CLH 队列配 AQS,读写模式能共舞;
独占抢锁是写线程,共享排队是读线程;
一个队列搞定它,公平高效没话说!


更多详细内容请关注其他相关文章!

发表回复 0

Your email address will not be published. Required fields are marked *