深入理解 ReentrantReadWriteLock 源码:基于 AQS 的读写锁实现
2026/9/7 19:18:37 网站建设 项目流程

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

一、整体架构设计

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的,在构造时一次性确定为公平或非公平同步器,后续不可变,保证了线程安全。
  • readerLockwriterLock提前创建,避免了readLock()/writeLock()方法中的临时对象创建开销,且锁对象本身不持有可变状态,所有状态都在sync中。

三、同步器顶层抽象:Sync

Sync是整个读写锁的核心,它继承AbstractQueuedSynchronizer。读锁和写锁的计数均用一个intstate表示:高 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_UNIT1 << 16,即读锁每增加一次计数,state0x10000,这样高 16 位+1。
  • MAX_COUNT0x0000FFFF(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

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

读锁获取:tryAcquireShared

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

写锁释放:tryRelease

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

读锁释放:tryReleaseShared

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

五、公平同步器:FairSync

公平模式下,所有线程必须严格按队列顺序获取锁。FairSync的实现只需在tryAcquiretryAcquireShared中增加对前驱节点的检查。代码框架如下(具体逻辑与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 源码继续深入,彻底掌握这一经典并发工具的实现。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询