ReentrantReadWriteLock · JUC
ReentrantReadWriteLock 源码分析
读多写少的场景下,独占锁把互不冲突的读线程也串行化了。ReentrantReadWriteLock 用 AQS 的一个 state 同时管两把锁:读锁共享、写锁独占,写锁还能”降级”成读锁。核心技巧是把 state 按位切成两半,一次 CAS 同时维护读写两个计数。
版本注意:大家常说的「高 16 位读计数、低 16 位写计数」是 JDK 8~24 的 int state 布局(各 16 位,最大计数 65535)。
本地 JDK 25 源码已把 Sync 换成AbstractQueuedLongSynchronizer,state 是 long,切分点从 16 变成 32,MAX_COUNT 提升到Integer.MAX_VALUE——位运算和整个机制一字不差,只是每半边更宽了。下面代码以本地源码为准。
// 基于 JDK 25 (本地 D:/1ForCode/JAVA_Source), java.util.concurrent.locks.ReentrantReadWriteLock
abstract static class Sync extends AbstractQueuedLongSynchronizer { // JDK ≤24 继承的是 int 版 AQS
static final int SHARED_SHIFT = 32; // JDK ≤24 这里是 16
static final long SHARED_UNIT = (1L << SHARED_SHIFT); // 读计数 +1 = state 加这一个单位
static final long MAX_COUNT = Integer.MAX_VALUE; // JDK ≤24 是 (1<<16)-1 = 65535
static final long EXCLUSIVE_MASK = (1L << SHARED_SHIFT) - 1;
// 高半段 = 总读锁计数(所有线程加起来), 低半段 = 写锁重入次数(只可能属于一个线程)
static int sharedCount(long c) { return (int)(c >>> SHARED_SHIFT); }
static int exclusiveCount(long c) { return (int)(c & EXCLUSIVE_MASK); }
// state 里只有"总读计数", 每个线程各自重入了几次, 得靠 ThreadLocal 单独记
static final class HoldCounter {
int count; // initially 0
// Use id, not reference, to avoid garbage retention
final long tid = LockSupport.getThreadId(Thread.currentThread()); // 存 tid 不存 Thread 引用, 防止 ThreadLocal 值反向拽住线程对象
}
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
private transient ThreadLocalHoldCounter readHolds; // 兜底: 每线程读重入计数
private transient HoldCounter cachedHoldCounter; // 缓存"最后一个成功拿读锁的线程"的计数器, 省一次 ThreadLocal 查找; 非 volatile, 良性数据竞争
private transient Thread firstReader; // 把读计数从 0 变 1 的那个线程, 单读者场景连 ThreadLocal 都不用碰
private transient int firstReaderHoldCount;
abstract boolean readerShouldBlock(); // 公平/非公平唯一的分歧点: 明明能抢, 要不要让
abstract boolean writerShouldBlock();
// 写锁获取: 独占语义, 走 AQS 的 tryAcquire
protected final boolean tryAcquire(long acquires) {
Thread current = Thread.currentThread();
long c = getState();
long w = exclusiveCount(c);
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
if (w == 0 || current != getExclusiveOwnerThread())
return false; // w==0 即有读锁在场 → 写锁必须等; 哪怕读锁是自己持有的也一样失败 —— 这就是"锁升级"会死锁的代码根源
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires); // 写锁重入, 只有自己能到这, 普通写即可
return true;
}
if (writerShouldBlock() || // c==0 锁全空: 公平版还要看队列, 非公平版直接抢
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
protected final boolean tryRelease(long releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
long nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0; // 只看低半段: 降级场景下高半段还挂着自己的读计数, 写锁照样能放
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
}// 基于 JDK 25 (本地 D:/1ForCode/JAVA_Source), java.util.concurrent.locks.ReentrantReadWriteLock.Sync
// 读锁获取: 共享语义, 走 AQS 的 tryAcquireShared, 返回 >=0 表示成功
protected final long tryAcquireShared(long unused) {
Thread current = Thread.currentThread();
long c = getState();
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1L; // 别人持写锁 → 读失败; 写锁是自己的则放行 —— 锁降级的入口就在这个条件里
int r = sharedCount(c);
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) { // 整个 long 一起 CAS, 读计数 +1 的同时天然校验了"写计数没变"
if (r == 0) {
firstReader = current; // 第一个读者: 最快路径, 零 ThreadLocal 开销
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++; // 第一个读者重入
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null ||
rh.tid != LockSupport.getThreadId(current))
cachedHoldCounter = rh = readHolds.get(); // 缓存未命中才查 ThreadLocal
else if (rh.count == 0)
readHolds.set(rh); // 缓存里是自己但 ThreadLocal 已被 remove 过, 补回去
rh.count++;
}
return 1L;
}
return fullTryAcquireShared(current); // 快路径失败(该让/CAS 输/计数满), 转完整版重试循环
}
final long fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
long c = getState();
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
// else we hold the exclusive lock; blocking here
// would cause deadlock. // 持写锁的自己若在这被挡, 就没人能放写锁了 → 降级必须无条件放行
} else if (readerShouldBlock()) {
// Make sure we're not acquiring read lock reentrantly
if (firstReader == current) { // 重入的读者即使"该让"也放行: 它可能挡着后面的写者, 让它阻塞同样会死锁
// assert firstReaderHoldCount > 0;
} else {
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null ||
rh.tid != LockSupport.getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove(); // 白查了一次, 立刻清掉, 别给线程留一个空 HoldCounter
}
}
if (rh.count == 0)
return -1L; // 首次获取且政策要求让路 → 老实进队
}
}
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
if (compareAndSetState(c, c + SHARED_UNIT)) {
// ... 与 tryAcquireShared 相同的 firstReader/cachedHoldCounter 记账, 略
return 1L;
}
}
}
protected final boolean tryReleaseShared(long unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null ||
rh.tid != LockSupport.getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove(); // 重入数归零就从 ThreadLocal 移除, 防内存泄漏
if (count <= 0)
throw unmatchedUnlockException(); // 没拿过读锁却来解锁
}
--rh.count;
}
for (;;) {
long c = getState();
long nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc)) // 多个读者并发释放, 必须 CAS 自旋(对比写锁释放的普通写)
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
return nextc == 0; // 只有读写全空才返回 true 去唤醒后继 —— 唤醒的通常是等疯了的写者
}
}// 基于 JDK 25 (本地 D:/1ForCode/JAVA_Source), java.util.concurrent.locks.ReentrantReadWriteLock
static final class NonfairSync extends Sync {
final boolean writerShouldBlock() {
return false; // writers can always barge // 写者永远可以插队
}
final boolean readerShouldBlock() {
/* As a heuristic to avoid indefinite writer starvation,
* block if the thread that momentarily appears to be head
* of queue, if one exists, is a waiting writer. ... */
return apparentlyFirstQueuedIsExclusive(); // 队头等着的是写者 → 新读者让路。防写饥饿的启发式, 不是保证
}
}
static final class FairSync extends Sync {
final boolean writerShouldBlock() {
return hasQueuedPredecessors(); // 公平版读写一视同仁: 前面有人排队就不抢
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}原理串讲
一次典型的读锁获取:readLock().lock() → sync.acquireShared(1) → tryAcquireShared。
它先看低半段 exclusiveCount(c):有写锁且不是自己,直接返回 -1 去 AQS 排队;否则问 readerShouldBlock(),不用让就 compareAndSetState(c, c + SHARED_UNIT)——注意 CAS 的期望值是完整的 state,所以读计数 +1 的瞬间同时确认了写计数没被别人改过,这是位切分的第一个”为什么”:两个计数塞进一个原子变量,读写之间的互斥判断和计数更新合并成一次 CAS,不需要额外的锁去保护”两个计数的一致性”。
快路径失败(CAS 竞争输了、或政策说该让、或重入场景)才落到 fullTryAcquireShared 的完整循环,把重入判断从快路径里剥出去,是因为绝大多数读获取是非重入的,不值得让热路径每次都查 ThreadLocal。
state 高半段只是”全体读者的总数”,回答不了”我这个线程重入了几次”——但 unlock() 必须校验你真的持有,getReadHoldCount() 也要按线程报数。
于是有了三级记账:firstReader 记第一个读者(单读者场景一次 ThreadLocal 都不碰),cachedHoldCounter 记最后一个成功获取的读者(获取和释放通常是同一个线程紧挨着发生,缓存命中率很高),都不命中才查 readHolds 这个 ThreadLocal。
HoldCounter 存 tid 而不存 Thread 引用、计数归零就 readHolds.remove(),两个细节都是在防 ThreadLocal 场景的内存滞留。
锁降级(持写 → 取读 → 放写)为什么被放行:tryAcquireShared 的失败条件是「有写锁 且 持有者不是自己」,自己持写时取读锁畅通无阻;随后 tryRelease 只检查 exclusiveCount(nextc) == 0,高半段挂着自己的读计数完全不妨碍写锁释放。
反过来锁升级(持读 → 取写)必然卡死:tryAcquire 里 c != 0 && w == 0 意味着有读锁在场,不管这读锁是不是自己的都返回 false,线程进 AQS 队列 park,等读计数清零——可它自己那份读计数永远等不到自己来释放。
更糟的是两个读者同时升级,互相等对方放读锁,谁也醒不来。为什么不特判”读锁全是我自己的就允许升级”?因为升级必须是原子的排他动作,两个线程同时符合”特判”就会同时等对方,JUC 干脆在语义上禁止,把死锁风险从运行时提前到设计期。
写饥饿问题落在 readerShouldBlock 上。读锁是共享的,只要读者源源不断,state 高半段永远非零,写者的 tryAcquire 永远看到 c != 0 && w == 0。
非公平模式的解法是 apparentlyFirstQueuedIsExclusive():新读者抢锁前瞄一眼队头,如果队头等着的是写者就主动让路,让写者有机会插进读者流里。
这只是概率性缓解——写者排在几个读者后面时新读者照样插队。公平模式则读写都走 hasQueuedPredecessors(),严格 FIFO,写者不会饿死,代价是读吞吐明显下降。
设计取舍
- 一个 state 切两半,换来读写计数的单次 CAS 原子更新;代价是每半边计数上限缩水(JDK ≤24 只有 65535,JDK 25 换 long 后放宽到 Integer.MAX_VALUE)。
- 禁止锁升级不是实现偷懒,是语义上避免”两个升级者互等”的必然死锁;需要升级的场景应该先放读锁再抢写锁,并接受中间窗口数据可能已变。
- 锁降级的价值:写完后继续持读,保证自己刚写的数据在读期间不被别的写者改掉,又尽早把写锁让出去恢复读并发。
- firstReader/cachedHoldCounter 是典型的”为常见路径加缓存”:单读者和”谁获取谁释放”两种模式覆盖了绝大多数用法,靠良性数据竞争省掉 ThreadLocal 查找。
- 非公平模式写者可插队 + 队头写者优先的启发式,是吞吐和写饥饿之间的折中;真读多写少到极致(读者永不断流),该考虑 StampedLock 的乐观读而不是调公平参数。