欢迎光临
我们一直在努力

深入理解 ReentrantReadWriteLock 源码:基于 AQS 的读写锁实现

在多线程并发编程中,读写锁(ReadWriteLock)是一种重要的同步工具,它允许多个读线程同时访问共享资源,但写线程必须独占访问。JDK 提供了 ReentrantReadWriteLock,它基于 AQS(AbstractQueuedSynchronizer)实现,支持可重入、公平/非公平模式,并巧妙地利用一个 int 型 state 变量同时维护读锁和写锁的计数。本文将深入剖析其核心源码,帮助读者透彻理解读写锁的设计精髓。

一、整体架构设计

ReentrantReadWriteLock 实现了 ReadWriteLock 接口,对外暴露 readLock() 和 writeLock() 两个方法。其内部结构可以用“一个同步器 + 两把门面锁”来概括:

  • Sync:继承 AQS 的抽象同步器,承载所有同步逻辑(如获取/释放、公平策略、读写计数)。
  • NonfairSync / FairSync:实现具体的获取规则(非公平或公平)。
  • ReadLock / WriteLock:实现 Lock 接口的门面类,将对锁的操作(lock()、unlock())委托给同一个 Sync 实例。

这种设计使得读锁和写锁共享同一个等待队列,且读写互斥、写写互斥、读读共享的控制都集中在 Sync 中,避免了状态分散。

二、核心字段与构造方法

我们直接给出 ReentrantReadWriteLock 的完整源码框架,重点关注其字段和构造方法的设计:

public class ReentrantReadWriteLock implements ReadWriteLock { // 两把锁引用,构造方法内实例化 private final ReadLock readerLock; private final WriteLock writerLock; // 唯一同步器,Sync是AQS子类 final Sync sync; // 默认非公平构造 public ReentrantReadWriteLock() { this(false); } public ReentrantReadWriteLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); // 提前创建读写锁对象,传入自身this引用 readerLock = new ReadLock(this); writerLock = new WriteLock(this); } // getter方法,直接返回已经new好的对象,不会临时创建 @Override public Lock readLock() { return readerLock; } @Override public Lock writeLock() { return writerLock; } // … 内部类 Sync、NonfairSync、FairSync、ReadLock、WriteLock 将在下文展开}

设计亮点:

  • sync 字段是 final 的,在构造时一次性确定为公平或非公平同步器,后续不可变,保证了线程安全。
  • readerLock 和 writerLock 提前创建,避免了 readLock() / writeLock() 方法中的临时对象创建开销,且锁对象本身不持有可变状态,所有状态都在 sync 中。

三、同步器顶层抽象:Sync

Sync 是整个读写锁的核心,它继承 AbstractQueuedSynchronizer。读锁和写锁的计数均用一个 int 型 state 表示:高 16 位存放读锁持有计数,低 16 位存放写锁重入计数。

abstract static class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = 6317671515683782265L; // 移位常量,state高低16位拆分 static final int SHARED_SHIFT = 16; static final int SHARED_UNIT = (1 << SHARED_SHIFT); static final int MAX_COUNT = (1 << SHARED_SHIFT) – 1; static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) – 1; // 获取读锁计数(高16位) static int sharedCount(int c) { return c >>> SHARED_SHIFT; } // 获取写锁计数(低16位) static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; } // 写锁独占获取 protected abstract boolean tryAcquire(int acquires); // 读锁共享获取 protected abstract int tryAcquireShared(int acquires); // 写锁释放 protected abstract boolean tryRelease(int releases); // 读锁释放 protected abstract boolean tryReleaseShared(int releases);}

关键解析:

  • sharedCount(int c):无符号右移 16 位,得到当前所有线程持有的读锁次数(每个线程可多次重入,总体计数)。
  • exclusiveCount(int c):与掩码 0x0000FFFF 做与运算,得到写锁重入次数。
  • SHARED_UNIT 为 1 << 16,即读锁每增加一次计数,state 加 0x10000,这样高 16 位+1。
  • MAX_COUNT 为 0x0000FFFF(65535),表示读锁和写锁的最大重入次数限制。

四个抽象方法留给子类(公平/非公平)实现,体现了模板方法模式。

四、非公平同步器:NonfairSync

非公平模式是 ReentrantReadWriteLock 的默认策略,它允许新来的线程“插队”,以减少上下文切换开销。以下是 NonfairSync 的核心实现:

static final class NonfairSync extends Sync { private static final long serialVersionUID = -8159625535654395037L; @Override protected boolean tryAcquire(int acquires) { // 写锁获取实现 int state = getState(); int w = exclusiveCount(state); if (w != 0) { // 已经持有写锁,判断是否当前线程(可重入) if (getExclusiveOwnerThread() == Thread.currentThread()) { setState(state + acquires); return true; } return false; } // 当前无写锁,尝试CAS抢占写锁 if (sharedCount(state) != 0 || !compareAndSetState(state, state + acquires)) { return false; } setExclusiveOwnerThread(Thread.currentThread()); return true; } @Override protected int tryAcquireShared(int unused) { // 读锁获取核心逻辑 int state = getState(); // 存在其他线程持有写锁,读锁直接失败 if (exclusiveCount(state) != 0 && getExclusiveOwnerThread() != Thread.currentThread()) { return -1; } if (!readerShouldBlock() && compareAndSetState(state, state + SHARED_UNIT)) { return 1; } return fullTryAcquireShared(state); } // 判断新来读锁是否应当阻塞(防止写饥饿核心方法) final boolean readerShouldBlock() { Node h = head; Node s = h.next; // 后继第一个节点是独占写节点 → 新来读锁要排队 return s != null && !s.isShared(); } final int fullTryAcquireShared(int currState) { // 完整重试获取读锁逻辑,省略细节 return -1; } @Override protected boolean tryRelease(int releases) { // 写锁释放逻辑 int nextc = getState() – releases; if (exclusiveCount(nextc) == 0) { setExclusiveOwnerThread(null); setState(nextc); return true; } setState(nextc); return false; } @Override protected boolean tryReleaseShared(int unused) { // 读锁释放逻辑,循环CAS减少读计数 for (;;) { int state = getState(); int nextc = state – SHARED_UNIT; if (compareAndSetState(state, nextc)) { return nextc == 0; } } }}

写锁获取:tryAcquire

  • 若当前已存在写锁(w != 0),则判断持有者是否为当前线程,是则可重入,state 加 acquires(低 16 位增加)。
  • 若无线程持有写锁,但存在读锁(sharedCount(state) != 0),写锁获取失败(读写互斥);否则通过 CAS 尝试将 state 增加 acquires,成功则设置独占线程。
  • 读锁获取:tryAcquireShared

  • 若其他线程持有写锁,则返回 -1 失败(读写互斥);但若当前线程持有写锁,读锁可降级获取成功(锁降级)。
  • 在非公平模式下,先调用 readerShouldBlock() 判断是否需要阻塞:如果 AQS 等待队列的头部后继节点是独占模式(写节点),为避免写线程饥饿,新来的读线程必须排队。
  • 若无需阻塞,CAS 尝试将 state 增加 SHARED_UNIT(高 16 位+1),成功则返回 1。
  • CAS 失败或需要阻塞,则进入 fullTryAcquireShared 自旋重试。
  • 防止写饥饿的巧妙设计:readerShouldBlock() 检查队列头部是否存在等待的写节点,若有则让读线程乖乖排队,从而给写线程让出机会。

    写锁释放:tryRelease

    直接减少写锁计数,当低 16 位变为 0 时,将独占线程清空并返回 true,表示写锁完全释放,可唤醒后继节点。

    读锁释放:tryReleaseShared

    通过无限循环 CAS 将 state 减 SHARED_UNIT,直到成功。若释放后读计数为 0(即 nextc == 0),返回 true 表示无读线程占用,可允许后继写线程获取。

    五、公平同步器:FairSync

    公平模式下,所有线程必须严格按队列顺序获取锁。FairSync 的实现只需在 tryAcquire 和 tryAcquireShared 中增加对前驱节点的检查。代码框架如下(具体逻辑与 NonfairSync 类似,仅加入 hasQueuedPredecessors() 判断):

    static final class FairSync extends Sync { private static final long serialVersionUID = -1828979586444678633L; @Override protected boolean tryAcquire(int acquires) { // 公平写锁,先判断队列是否有前驱节点 return false; } @Override protected int tryAcquireShared(int acquires) { // 公平读锁实现 return -1; } @Override protected boolean tryRelease(int releases) { return false; } @Override protected boolean tryReleaseShared(int releases) { return false; }}注意:本文为聚焦核心思想,公平版本的方法体均返回默认值,读者可自行参考 JDK 源码补充完整逻辑。

    六、锁的门面类:ReadLock 与 WriteLock

    这两个内部类实现了 Lock 接口,但不直接维护任何同步状态,而是将所有操作委托给外部类的 sync 对象。这使得读写锁能共用同一个等待队列,保证读写互斥的正确性。

    读锁 ReadLock

    public static class ReadLock implements Lock { private final ReentrantReadWriteLock rwLock; protected ReadLock(ReentrantReadWriteLock rw) { rwLock = rw; } @Override public void lock() { rwLock.sync.acquireShared(1); } @Override public void unlock() { rwLock.sync.releaseShared(1); }}

    • lock() 调用 AQS 的 acquireShared,该方法内部会先调用 tryAcquireShared,失败则进入同步队列等待。
    • unlock() 调用 releaseShared,最终触发 tryReleaseShared 并唤醒后续节点。

    写锁 WriteLock

    public static class WriteLock implements Lock { private final ReentrantReadWriteLock rwLock; protected WriteLock(ReentrantReadWriteLock rw) { rwLock = rw; } @Override public void lock() { rwLock.sync.acquire(1); } @Override public void unlock() { rwLock.sync.release(1); }}

    • lock() 调用 AQS 的独占模式 acquire,其中 tryAcquire 由子类实现。
    • unlock() 调用 release,递减写计数并可能释放锁。

    七、锁升降级的支持与限制

    ReentrantReadWriteLock 明确支持锁降级(从写锁降级为读锁),但禁止锁升级(从读锁升级为写锁)。理解这两者的差异是掌握读写锁用法的关键。

    锁升级(读 → 写)被禁止

    1. 表面原因:读写并发冲突如果允许多个线程同时持有读锁,而其中一个线程试图升级获取写锁,一旦成功,就会出现“一个线程写、其他线程仍在读”的局面,直接违反读写互斥的语义。

    2. 真正致命的原因:死锁风险造成锁升级被禁用的根本原因,不仅仅是瞬时的读写冲突,而是一种典型的资源互相等待导致的死锁:

    • 线程 A、线程 B 同时持有读锁;
    • A 尝试升级写锁:必须等待所有读锁释放(包括 B 手中的读锁),于是 A 阻塞;
    • B 此时也尝试升级写锁:同样必须等待所有读锁释放(包括 A 手中的读锁),B 也阻塞。

    此时 A 与 B 各自握着一部分读锁不放,又都在等待对方释放,形成永久死锁。JDK 设计者选择直接禁用锁升级,从根源杜绝此类场景,而不仅仅是防止瞬时读写共存。

    锁降级(写 → 读)被允许

    写锁是独占的,同一时刻只有一个线程持有写锁。该线程若在持有写锁期间继续获取读锁,因为不存在其他并发持有读锁的线程,不会产生读写冲突,也不会引发死锁。标准流程为:获取写锁 → 获取读锁 → 释放写锁中间短暂阶段,该线程同时持有写锁和读锁(两种锁共存),最终效果是从独占写锁平滑过渡到共享读锁,保证数据的连续可见性。

    注意:锁降级必须严格遵循上述顺序,不能先释放写锁再获取读锁。若先释放写锁,中间可能被其他写线程插入修改数据,导致后续读到的数据不再一致。

    ReentrantReadWriteLock 在代码层面通过 tryAcquireShared 的判断逻辑实现了降级支持:

    if (exclusiveCount(state) != 0 && getExclusiveOwnerThread() != Thread.currentThread()) { return -1;}

    若持有写锁的正是当前线程,则允许继续获取读锁,从而安全地完成锁降级。

    区分两个概念:共存与锁升降级

    经常有人将“读写锁共存”与“锁升降级”混淆,需要明确:

    • 锁共存:指同一时刻系统中同时存在读锁和写锁。由于读写互斥,正常情况下不允许共存;唯一的例外是锁降级的中间状态,即一个线程同时持有写锁和读锁(自己和自己共存)。
    • 锁升降级:指线程主动改变锁的模式。升级是从读到写,降级是从写到读。前者被禁止,后者被允许。

    理解这两者的区别,有助于在面试和实际开发中准确阐述 ReentrantReadWriteLock 的设计约束。

    八、总结

    ReentrantReadWriteLock 通过一个精妙的 state 高低位拆分、两个门面锁的委托模式,以及 AQS 的模板方法,实现了高效的读写锁语义。其非公平模式还通过 readerShouldBlock() 巧妙避免写线程饥饿。理解这套源码,不仅有助于正确使用读写锁,更能加深对 AQS 同步器框架的掌握。

    核心要点回顾:

    • 状态拆分:高 16 位读计数,低 16 位写计数。
    • 读写互斥:存在写锁时其他线程的读/写均被阻塞,但当前线程可降级获取读锁。
    • 非公平策略:新写线程可能直接插队;读线程则需观察队列是否已有等待的写节点。
    • 可重入性:写锁通过 getExclusiveOwnerThread 判断重入;读锁通过每线程维护重入计数(不在本文展开,可参考完整源码)。
    • 锁升降级规则:锁升级被禁止,避免死锁和读写冲突;锁降级被允许,遵循“获取写锁 → 获取读锁 → 释放写锁”的顺序保障数据可见性。

    本文提供的代码为 ReentrantReadWriteLock 核心逻辑的骨架版本,省略了部分细节(如 fullTryAcquireShared 的完整自旋、每个线程的读锁重入计数等),但已完整呈现了其设计脉络。希望读者能结合 JDK 源码继续深入,彻底掌握这一经典并发工具的实现。

    赞(0)
    未经允许不得转载:171主机测评 » 深入理解 ReentrantReadWriteLock 源码:基于 AQS 的读写锁实现
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址