在多线程并发编程中,读写锁(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
读锁获取:tryAcquireShared
防止写饥饿的巧妙设计: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 源码继续深入,彻底掌握这一经典并发工具的实现。