并发编程(八):读写锁——从语言规则到 CPU
目录
- 0. 这一篇继续回答什么?
- 1. Java ReadWriteLock 在语言层保证什么?
- 2. JDK 如何实现 ReentrantReadWriteLock?
- 3. Go Runtime 如何实现 sync.RWMutex?
- 4. CPython:为什么标准库没有 Read-Write Lock?
- 5. 两种实现的共同套路
- 6. ReadWriteLock 和 Mutex 的关系
- 7. 下一篇:AQS / AQLS——Java 锁的统一同步框架
0. 这一篇继续回答什么?
上一篇最后回到 Mutex:它能保护一整段临界区,但无论读还是写,同一时刻都只允许一个执行单元进入。
如果两个线程都只读取共享状态,它们其实没有必要互相等待:
Thread A Thread B
read counter read counter
读写锁就是在 Mutex 的基础上进一步区分 Reader 和 Writer:
Read + Read → 可以并发
Read + Write → 互斥
Write + Write → 互斥
这一篇继续沿用前面的两个例子:
counter++:看 Write Lock 如何保证 Atomicity;counter + ready:看 Write Lock → Read Lock 如何建立 Visibility 和 Ordering。
同样先从语言规则解释保证是什么,再继续往下看 JDK、HotSpot 和 CPU 如何实现。
1. Java ReadWriteLock 在语言层保证什么?
1.1 Atomicity
Java ReadWriteLock 对 Reader / Writer 的边界定义得很直接:
“The read lock may be held simultaneously by multiple reader threads, so long as there are no writers.”
同时:
“The write lock is exclusive.”
因此,读取可以并发,但写入仍然需要独占。
还是用 counter++:
ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
Lock writeLock = rw.writeLock();
writeLock.lock();
try {
counter++;
} finally {
writeLock.unlock();
}
两个线程同时更新时:
初始:counter = 0
Thread A Thread B
writeLock.lock()
writeLock.lock()
等待
read counter -> 0
counter + 1 -> 1
write counter -> 1
writeLock.unlock()
writeLock.lock() 返回
read counter -> 1
counter + 1 -> 2
write counter -> 2
writeLock.unlock()
最终:
counter = 2
这里和 Mutex 一样,Atomicity 来自 Write Lock 把冲突的临界区串行化,不是 counter++ 本身变成了原子指令。
Read Lock 只应该保护读取。多个 Reader 可以同时进入,因此不能靠 Read Lock 保护共享写入。
1.2 Visibility 和 Ordering
ReadWriteLock 还规定了 Write Lock 与关联 Read Lock 之间的内存同步语义:
“A thread successfully acquiring the read lock will see all updates made upon previous release of the write lock.”
还是套回同一个 counter / ready:
int counter = 0;
boolean ready = false;
ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
Lock readLock = rw.readLock();
Lock writeLock = rw.writeLock();
// Thread A
writeLock.lock();
try {
counter = 1;
ready = true;
} finally {
writeLock.unlock();
}
// Thread B
readLock.lock();
try {
if (ready) {
System.out.println(counter);
}
} finally {
readLock.unlock();
}
如果 B 在 A 释放 Write Lock 之后成功获得 Read Lock:
Thread A Thread B
counter = 1
│
│ program order
▼
ready = true
│
▼
writeLock.unlock()
│
│ memory synchronization
▼
readLock.lock() returns
│
│ program order
▼
read ready == true
read counter
因此:
write counter
↓ happens-before
write ready
↓
writeLock.unlock
↓ memory synchronization
readLock.lock returns
↓ happens-before
read counter
所以当 B 已经获得后续 Read Lock 并读到 ready == true 时,前面的 counter = 1 对 B 可见,不能出现:
ready == true
counter == 0
这里同时得到两个保证:
- Visibility:Writer 在释放 Write Lock 之前完成的写入,对后续获得 Read Lock 的 Reader 可见;
- Ordering:Writer 的写入、Write Lock 的释放、Read Lock 的获取和 Reader 的读取形成 happens-before 顺序。
2. JDK 如何实现 ReentrantReadWriteLock?
2.1 分层
先看实现分层:
Java Source Code
实例:readLock.lock() / writeLock.lock()
作用:声明读临界区与写临界区
│
▼
JVM Bytecode
实例:invokeinterface Lock.lock / Lock.unlock
作用:调用 ReadLock / WriteLock API
│
▼
Runtime / Language Implementation
实例:ReentrantReadWriteLock.Sync / AbstractQueuedLongSynchronizer
+ Unsafe CAS / LockSupport
作用:实现 Reader / Writer 状态、竞争、排队与唤醒
│
▼
OS Thread
实例:Runnable / Parked Java Thread
作用:竞争失败时进入阻塞,收到 unpark 后重新参与竞争
│
▼
x86-64 Hardware
实例:Atomic Instruction / Cache Coherence / Memory Ordering
作用:提供 Atomicity、Visibility 与 Ordering 的硬件基础
2.2 一次 Write Lock → Read Lock 的完整实现路径
继续使用 counter / ready,先由 Thread A 写入,再由 Thread B 读取:

上面的时序图只保留跨层主流程。Runtime 内部的 Reader / Writer 竞争路径可以进一步展开为:

这里有两个核心状态:
high 32 bits
→ shared count
→ Reader 数量
low 32 bits
→ exclusive count
→ Writer 重入次数
当前 JDK 25 的 ReentrantReadWriteLock.Sync 继承 AbstractQueuedLongSynchronizer。Reader 走 Shared 模式,Writer 走 Exclusive 模式;两者最终都竞争同一个 64-bit state。
相关实现可以查看 OpenJDK 25 的 ReentrantReadWriteLock.java 和 AbstractQueuedLongSynchronizer.java。
2.3 Runtime / Language Implementation 层的三种保证
2.3.1 Atomicity
Atomicity 对应流程图里的状态竞争:
Writer
→ CAS exclusive count
→ 成功后独占写临界区
Reader
→ CAS shared count
→ 多个 Reader 可以分别成功
Writer 获取锁时,如果已经存在 Reader,或者 Write Lock 被其他线程持有,获取失败。
如果当前线程本来就持有 Write Lock,则属于可重入获取,直接增加 exclusive count。
Reader 获取锁时,如果其他线程持有 Write Lock,获取失败;否则通过 CAS 增加 shared count。
因此 Runtime 真正维护的是:
Read + Read
→ 允许多个 Shared acquire
Read + Write
→ 冲突
Write + Write
→ 冲突
2.3.2 Visibility 和 Ordering
AbstractQueuedLongSynchronizer 的 state 是 volatile long:
private volatile long state;
它的基础操作具有:
getState()
→ volatile read
setState()
→ volatile write
compareAndSetState()
→ volatile read + volatile write
Write Lock 释放时更新 state;后续 Reader 获取锁时读取并 CAS 同一个 state。
所以:
Writer writes
↓
release Write Lock
↓
volatile / CAS state synchronization
↓
acquire Read Lock
↓
Reader reads
这就是语言层 Visibility / Ordering 在 Runtime / Language Implementation 层的落点。
2.4 Hardware 层的三种保证
继续向下,仍然回到三类硬件能力:
Atomicity
→ Atomic Instruction
→ x86-64:LOCKed CAS 等原子 RMW
Visibility
→ Cache Coherence
→ 让其他 CPU Core 不再继续使用已经失效的旧 Cache Line
Ordering
→ x86-64 Memory Ordering + LOCKed RMW / 必要的顺序约束
→ 保证锁状态变化前后的内存操作顺序
compareAndSetState() 最终通过 Unsafe.compareAndSetLong 落到目标架构的原子 CAS。
读写锁没有新的硬件原语。它是在 Atomic、线程等待 / 唤醒之上,编码出“多个 Reader 或一个 Writer”这套规则。
3. Go Runtime 如何实现 sync.RWMutex?
Go 标准库直接提供 sync.RWMutex。
它和 Java ReadWriteLock 具有相同的核心 Reader / Writer 互斥模型:多个 Reader 可以并发,一个 Writer 必须独占;完整 API 语义并不完全相同。
两者最重要的差异是:
Java ReentrantReadWriteLock |
Go sync.RWMutex |
|
|---|---|---|
| Read Lock 重入 | 支持 | 不支持递归 RLock |
| Write Lock 重入 | 支持 | 不支持 |
| Write → Read 降级 | 支持 | 不支持 |
| Read → Write 升级 | 不支持 | 不支持 |
| Fairness | 可选 fair / nonfair | 不提供 fair 模式 |
Go Memory Model 还明确规定:
“For any call to RLock, there exists an n such that the n'th call to Unlock ‘synchronizes before’ that call to RLock.”
3.1 分层
Go Source Code
实例:rw.RLock() / rw.Lock()
作用:声明读临界区与写临界区
│
▼
Go Implementation
实例:sync.RWMutex / sync.Mutex / sync/atomic / Runtime Semaphore
作用:实现 Reader / Writer 状态、竞争、等待与唤醒
│
▼
Goroutine / Scheduler
实例:Runnable / Parked Goroutine
作用:竞争失败时 Park Goroutine,Wakeup 后重新调度
│
▼
x86-64 Hardware
实例:Atomic Instruction / Cache Coherence / Memory Ordering
作用:提供 Atomicity、Visibility 与 Ordering 的硬件基础
Go 这里和 Java 的关键区别是:runtime_SemacquireRWMutex* Park 的是 Goroutine,不是直接阻塞一个固定的 OS Thread。
3.2 一次 Write Lock → Read Lock 的完整实现路径
继续使用 counter / ready:
var counter int
var ready bool
var rw sync.RWMutex
先由 Goroutine A 写入,再由 Goroutine B 读取:

上面的时序图只保留跨层主流程。Go Implementation 内部的 Reader / Writer 竞争路径可以进一步展开为:

核心状态是:
w
→ Writer 之间互斥
readerCount
→ >= 0:当前 Reader 数量
→ < 0:已经有 Writer pending
readerWait
→ Writer 还需要等待多少 Reader 离开
readerSem / writerSem
→ Reader / Writer 的等待与唤醒
相关实现可以查看 Go 源码的 sync/rwmutex.go。
3.3 Runtime / Language Implementation 层的三种保证
3.3.1 Atomicity
Atomicity 对应上面流程图里的两个核心状态操作。
Reader 进入时:
readerCount.Add(1)
这个更新必须是 Atomic。多个 Reader 可以分别成功增加计数。
Writer 进入时,先通过内部 w Mutex 保证 Writer 之间互斥,再执行:
readerCount.Add(-rwmutexMaxReaders)
把状态切到 “Writer pending”。
因此:
Read + Read
→ 允许多个 Reader
Read + Write
→ Writer pending 后,新 Reader 被阻塞
Write + Write
→ 先由 w Mutex 串行化
这里不是用一个统一的 state 编码所有状态,而是组合 Mutex + Atomic Counter + Semaphore 实现同一套 Reader / Writer 规则。
3.3.2 Visibility 和 Ordering
Go 对 RWMutex 的同步关系由 API 直接定义,而不是从 readerCount 的 Atomic 操作单独推导。
官方规则包括:
Unlock
→ synchronized-before
→ 后续对应的 RLock
RUnlock
→ synchronized-before
→ 后续对应的 Lock
还是套回 counter / ready:
Goroutine A Goroutine B
counter = 1
ready = true
│
▼
Unlock() ── synchronized-before ──► RLock() returns
│
▼
read ready == true
read counter == 1
因此,当 B 通过这次同步获得 Read Lock 后,A 在 Write Lock 内完成的写入对 B 可见,并保持正确的顺序。
实现上,Mutex、Runtime Semaphore 和 Atomic 状态共同完成这条同步路径;不能把 readerCount.Add 本身理解成 Reader 之间的同步关系。
3.4 Hardware 层的三种保证
继续向下,最终仍然是前面几篇的三类硬件能力:
Atomicity
→ Atomic Instruction
→ x86-64:LOCK XADDL / LOCK CMPXCHGL 等原子 RMW
Visibility
→ Cache Coherence
→ 让其他 CPU Core 不再继续使用已经失效的旧 Cache Line
Ordering
→ x86-64 Memory Ordering + LOCKed RMW
→ 提供同步路径需要的顺序约束
readerCount.Add / readerWait.Add 最终会落到 x86-64 的原子加法路径;内部 w Mutex 的状态竞争则继续复用前面 Mutex 已经展开过的 CAS / Atomic 路径。
所以 Go RWMutex 也没有新的硬件原语。它只是用 Atomic、Mutex 和 Runtime Semaphore 组合出 Reader / Writer 规则。
4. CPython:为什么标准库没有 Read-Write Lock?
Python 不是做不了 Read-Write Lock。事实上,CPython 很早就有过把 RWLock 加进 threading 的提案:Issue 8800。
这个提案最终没有进入标准库。从 Issue 8800 的长期讨论来看,至少有两个明显阻力。
1. 传统 CPython 下,GIL 让 Read-Write Lock 的收益有限
当时 CPython 开发者直接指出:
“since the GIL serializes everything anyway, this isn't likely to benefit many situations”
核心问题很简单:
Read-Write Lock 的主要收益
→ 多个 Reader 真正并行
传统 CPython
→ GIL 让 Python Code 同一时刻主要只有一个 Thread 执行
所以即使允许多个线程同时获得 Read Lock,它们通常也不能并行执行 Python Code。
这不表示 RWLock 完全没价值。I/O、释放 GIL 的 C Extension 等场景仍然可能受益。但对传统 CPython 的通用线程代码来说,它的收益没有 Java / Go 那么直接。
Free-threaded CPython 改变了这一点:没有 GIL 时,多个 Python Thread 可以真正并行执行,Reader 并发的价值明显提高。
所以第一点的前提正在变化。但即使 RWLock 的收益变得更明确,标准库仍然要面对第二个问题:API 和调度语义没有形成统一意见。
2. Issue 8800 对 API 和调度 Policy 没有形成统一意见
讨论里主要有两类争议。
第一类是 API 形态:
一个统一的 RWLock
还是
两个关联的 Shared / Exclusive Lock
例如,有开发者认为应该返回两个独立的 Shared / Exclusive Lock:
“having two different lock primitives ... would be much more flexible, pythonic”
也有开发者更倾向一个统一的 RWLock。
第二类是 调度 Policy:
Reader Priority / Writer Priority?
是否 Fair / FIFO?
Writer 等待时,新 Reader 能不能继续进入?
Read Lock 能不能升级成 Write Lock?
Write Lock 能不能降级成 Read Lock?
这些选择都会直接影响 RWLock 的行为和 API 语义,而 Issue 8800 没有形成一个统一方案。
所以问题不是实现不了,而是 标准库该固定哪一套 RWLock API 和 Policy,没有统一意见。一旦进入标准库,这些语义就需要长期保持兼容。
截至现在,threading 标准库仍然只提供 Lock、RLock、Condition、Semaphore、Event 等基础同步对象,没有加入 Read-Write Lock。
如果应用确实需要 RWLock,通常有两条路:
使用第三方 / 自定义 RWLock
│
└── 直接获得 Read Lock / Write Lock 语义
使用 threading.Lock + Condition
│
└── 自己定义 Reader / Writer 计数与优先级 Policy
早期的 Issue 8800 提案本身就是使用 Condition 和 Lock 组合实现 RWLock。
所以 Python 缺少的是 标准库统一定义的 RWLock API 与 Policy,不是缺少实现这种同步语义的能力。
5. 两种实现的共同套路
看完 Java 和 Go,再把具体实现拿掉,只看 ReadWriteLock 最终需要解决什么问题。
5.1 谁能进入?——原子维护 Reader / Writer 状态
Mutex 只需要回答“锁住了没有”。
ReadWriteLock 需要同时维护:
Reader Count
+
Writer State
Java:
64-bit state
→ shared count
→ exclusive count
Go:
readerCount
+
Writer Mutex
+
readerWait
编码方式不同,但进入条件相同:
Reader
→ 没有冲突的 Writer
Writer
→ 没有其他 Writer
→ 没有 Reader
这些状态变化都必须依靠 Atomic 操作完成,否则多个执行单元可能同时看到旧状态并错误进入。
5.2 冲突的人怎么办?——Park / Wakeup
状态检查失败以后,等待者不能无限空转。
两种实现最终都进入:
检查 Reader / Writer 状态
│
├── 可以进入
│ └── 更新状态后进入
│
└── 冲突
│
▼
排队 / 等待
│
▼
Park
│
▼
相关持有者释放
│
▼
Wakeup
│
└── Retry
具体等待对象不同:
Java
→ AQLS Sync Queue
→ LockSupport.park()
→ OS Thread Parked
Go
→ Runtime Semaphore
→ Park Goroutine
→ Scheduler 运行其他 Goroutine
所以共同套路是 Conflict → Park → Wakeup → Retry,只是 Java Park 的是 Thread,Go Park 的是 Goroutine。
5.3 后来的 Reader 为什么能看到前一个 Writer 的数据?
最后仍然是同步边界:
Writer writes
│
▼
Write Unlock [Release]
│
▼
Read Lock [Acquire]
│
▼
Reader reads
Java 通过 AQLS 的 volatile / CAS 状态更新落实这条边界。
Go 由 RWMutex 的 synchronized-before 规则定义语义,再由内部 Mutex、Runtime Semaphore 和 Atomic 路径落实。
因此两种实现最终都必须保证:
Writer 释放之前的写入,排在后续 Reader 获取锁之后的读取之前。
6. ReadWriteLock 和 Mutex 的关系
两者都保护临界区,区别在于是否区分 Reader 和 Writer:
| Mutex | ReadWriteLock | |
|---|---|---|
| 核心问题 | 一段临界区互斥 | 区分读临界区与写临界区 |
| Atomicity | 整段临界区互斥 | 冲突操作互斥;多个 Reader 可并发 |
| Visibility | 是 | 是 |
| Ordering | 是 | 是 |
| 读–读 | 互斥 | 可以并发 |
| 读–写 | 互斥 | 互斥 |
| 写–写 | 互斥 | 互斥 |
| 典型场景 | 通用共享状态保护 | 读多写少,且读临界区存在真实竞争 |
可以把关系简化成:
Mutex
→ 所有人都走一个 Exclusive 入口
ReadWriteLock
→ Reader 走 Shared 入口
→ Writer 走 Exclusive 入口
→ 只放开 Read + Read
ReadWriteLock 不是比 Mutex “更强”的锁。
它只是把 Mutex 的独占边界进一步细分,让互不冲突的 Reader 可以并发。
7. 下一篇:AQS / AQLS——Java 锁的统一同步框架
这一篇已经多次碰到:
ReentrantReadWriteLock
↓
Sync
↓
AbstractQueuedLongSynchronizer
它不是某一把锁自己的实现细节,而是 Java 并发锁体系里更底层的同步框架。
下一篇继续下钻 AQS 家族:
AQS / AQLS
→ state
→ Exclusive / Shared
→ CLH-style Queue
→ acquire / release
→ park / unpark
→ Condition Queue
其中 JDK 25 的 ReentrantReadWriteLock 实际使用的是 AbstractQueuedLongSynchronizer,它和 AbstractQueuedSynchronizer 的核心模型相同,只是同步状态使用 long。
Go 没有一个与 AQS 对称的统一框架。
Go 更倾向于让不同同步原语直接组合:
Atomic State
+
Mutex
+
Runtime Semaphore
+
Goroutine Scheduler
例如 sync.RWMutex 就直接组合 w、readerCount、readerWait、readerSem 和 writerSem,而不是统一继承某个 AQS 式 Synchronizer。
所以 AQS / AQLS 是 Java 这条实现链里非常重要的一层,也是下一篇单独展开的对象。
本文首发于 ThinkerQAQ 的个人博客,由作者本人同步发布。原文可能持续修订,最新版本请以个人博客为准。

浙公网安备 33010602011771号