深入解析ReentrantReadWriteLock读写锁机制
ReentrantReadWriteLock
提供了读写分离的锁机制,允许多个线程同时获取读锁,但写锁是独占的。它支持可重入、公平/非公平策略、锁降级等高级特性。
内部类结构
Sync
:抽象同步器,基于AbstractQueuedLongSynchronizer
ReadLock/WriteLock
:分别封装读写锁操作FairSync/NonfairSync
:公平性策略实现
线程安全保证
- 使用 CAS 操作保证状态更新的原子性
- 基于 AQS 的等待队列管理阻塞线程
- 通过
volatile
变量和内存屏障保证可见性
1. 核心能力概述
读写分离机制
- 读锁共享:多个线程可以同时持有读锁
- 写锁独占:只有一个线程可以持有写锁
- 读写互斥:读锁和写锁不能同时被持有
2. 关键特性详解
2.1 可重入性
// 支持同一线程多次获取同一类型的锁
if (firstReader == current) {firstReaderHoldCount++;
} else {// 通过 HoldCounter 记录每个线程的持有次数rh.count++;
}
2.2 公平性策略
- 非公平模式(默认):
NonfairSync
final boolean writerShouldBlock() {return false; // 写线程可以直接抢占 }
- 公平模式:
FairSync
final boolean writerShouldBlock() {return hasQueuedPredecessors(); // 检查是否有等待更久的线程 }
2.3 锁降级支持
支持从写锁降级到读锁,但不支持从读锁升级到写锁:
// 典型的锁降级模式
rwl.writeLock().lock();
try {// 更新数据rwl.readLock().lock(); // 在释放写锁前获取读锁
} finally {rwl.writeLock().unlock(); // 释放写锁,仍持有读锁
}
状态编码设计
使用一个 long
型变量同时记录读写锁状态:
static final int SHARED_SHIFT = 32;
static final long SHARED_UNIT = (1L << SHARED_SHIFT);
static final long EXCLUSIVE_MASK = (1L << SHARED_SHIFT) - 1;// 高32位:读锁计数
static int sharedCount(long c) { return (int)(c >>> SHARED_SHIFT); }
// 低32位:写锁计数
static int exclusiveCount(long c) { return (int)(c & EXCLUSIVE_MASK); }
写锁获取逻辑
写锁 (tryAcquire
) 的逻辑相对简单:
- 独占性: 写锁是独占的,任何时候最多只有一个线程可以持有写锁。
- 状态检查:
- 如果当前没有锁 (state == 0),则尝试通过 CAS 获取锁。如果失败(比如因为公平策略
writerShouldBlock()
返回 true,或者 CAS 竞争失败),tryAcquire
直接返回false
。后续的排队和唤醒逻辑由 AQS 框架处理。 - 如果当前已有锁 (state != 0):
- 如果存在读锁 (写计数
w
为 0),则获取失败。 - 如果存在写锁,但持有者不是当前线程,则获取失败。
- 如果当前线程已持有写锁(重入),则直接增加写锁计数。
- 如果存在读锁 (写计数
- 如果当前没有锁 (state == 0),则尝试通过 CAS 获取锁。如果失败(比如因为公平策略
- 重入处理: 写锁的重入计数直接记录在
state
的低位。判断重入和增加计数相对直接。 - AQS 框架: 对于获取失败的情况(例如,锁被其他线程持有,或 CAS 失败),AQS 框架会自动处理线程的入队、阻塞和后续的唤醒重试。
tryAcquire
只需要负责单次尝试获取的逻辑。
因此,写锁的 tryAcquire
方法已经能够完整地处理单次尝试获取锁的各种情况,包括重入。如果一次 tryAcquire
失败,AQS 会负责后续的流程,不需要一个额外的 "full" 版本来进行内部循环重试或处理更复杂的重入逻辑。
protected final boolean tryAcquire(long acquires) {Thread current = Thread.currentThread();long c = getState();long w = exclusiveCount(c);if (c != 0) {// 1. 如果有读锁或其他线程持有写锁,失败if (w == 0 || current != getExclusiveOwnerThread())return false;// 2. 重入检查if (w + exclusiveCount(acquires) > MAX_COUNT)throw new Error("Maximum lock count exceeded");setState(c + acquires);return true;}// 3. 尝试获取写锁if (writerShouldBlock() || !compareAndSetState(c, c + acquires))return false;setExclusiveOwnerThread(current);return true;
}
readHolds
和 cachedHoldCounter
位于 ReentrantReadWriteLock.java
中 Sync
类内部。
这两个字段都与读锁的重入计数有关。ReentrantReadWriteLock
允许同一个线程多次获取读锁(或写锁),这种行为称为重入。
-
readHolds
:ReentrantReadWriteLock.java
/*** The number of reentrant read locks held by current thread.* Initialized only in constructor and readObject.* Removed whenever a thread's read hold count drops to 0.*/private transient ThreadLocalHoldCounter readHolds;
- 类型:
readHolds
是一个ThreadLocalHoldCounter
类型的成员变量。ThreadLocalHoldCounter
继承自ThreadLocal<HoldCounter>
。这意味着每个线程都有自己独立的HoldCounter
副本。 HoldCounter
: 这个内部类非常简单,它包含两个字段:int count
: 表示当前线程持有读锁的重入次数。final long tid
: 当前线程的ID,用于避免在HoldCounter
对象生命周期结束后依然持有对Thread
对象的引用,从而防止内存泄漏。
/*** A counter for per-thread read hold counts.* Maintained as a ThreadLocal; cached in cachedHoldCounter.*/ static final class HoldCounter {int count; // initially 0// Use id, not reference, to avoid garbage retentionfinal long tid = LockSupport.getThreadId(Thread.currentThread()); }/*** ThreadLocal subclass. Easiest to explicitly define for sake* of deserialization mechanics.*/ static final class ThreadLocalHoldCounterextends ThreadLocal<HoldCounter> {public HoldCounter initialValue() {return new HoldCounter();} }
- 作用:
readHolds
的核心作用是精确地追踪和管理每个线程获取读锁的重入次数。当一个线程第一次获取读锁时,会为其创建一个HoldCounter
并存入其线程本地存储中;之后每次重入获取读锁,对应HoldCounter
的count
就会增加;每次释放读锁,count
就会减少。当一个线程的HoldCounter
的count
降为0时,表示该线程不再持有读锁,此时会从ThreadLocal
中移除这个HoldCounter
。
- 类型:
-
cachedHoldCounter
:ReentrantReadWriteLock.java
/*** The hold count of the last thread to successfully acquire* readLock. This saves ThreadLocal lookup in the common case* where the next thread to release is the last one to* acquire. This is non-volatile since it is just used* as a heuristic, and would be great for threads to cache.** <p>Can outlive the Thread for which it is caching the read* hold count, but avoids garbage retention by not retaining a* reference to the Thread.** <p>Accessed via a benign data race; relies on the memory* model's final field and out-of-thin-air guarantees.*/private transient HoldCounter cachedHoldCounter;
- 类型:
cachedHoldCounter
是一个HoldCounter
类型的成员变量。 - 作用: 这是一个性能优化的手段。它缓存了最后一个成功获取读锁的线程的
HoldCounter
。 - 优化原理: 在很多情况下,一个线程获取读锁后,紧接着就会释放该读锁。如果下一个释放读锁的线程恰好是上一个获取读锁的线程,那么就可以直接使用
cachedHoldCounter
中缓存的HoldCounter
,而无需通过readHolds.get()
进行ThreadLocal
的查找。ThreadLocal
的查找相对而言会有一些开销,通过这种缓存机制可以减少这种开销,尤其是在读锁竞争不激烈或者单个线程连续操作读锁的场景下。 - 非 volatile: 注释中明确指出它是非
volatile
的,因为它仅仅作为一个启发式的缓存。即使存在数据竞争(benign data race),最坏的情况也只是缓存失效,需要回退到ThreadLocal
查找,不影响正确性。 - 避免内存泄漏: 和
HoldCounter
内部使用tid
而不是直接引用Thread
对象一样,cachedHoldCounter
也只持有HoldCounter
,而HoldCounter
内部通过tid
间接关联线程,从而避免了因为缓存导致线程对象无法被垃圾回收的问题。
- 类型:
读锁获取逻辑
tryAcquireShared 的设计目标是快速路径:
- 它尝试以最小的开销处理最常见的情况:
- 非重入或简单重入 (当前线程是 firstReader)。
- 一次 CAS 成功。
- 它故意推迟了对复杂重入情况(非 firstReader 的重入,需要访问 ThreadLocal)的处理,以避免在常见路径上产生 ThreadLocal.get() 的开销。
protected final long tryAcquireShared(long unused) {Thread current = Thread.currentThread();long c = getState();// 1. 检查是否有其他线程持有写锁if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)return -1L;// 2. 尝试获取读锁int r = sharedCount(c);if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {// 成功获取,更新持有计数return 1L;}return fullTryAcquireShared(current);
}
当 tryAcquireShared
的快速路径失败时(例如 CAS 失败,或者需要处理复杂的重入逻辑),就会调用 fullTryAcquireShared
。这个方法包含了更完整的逻辑:
- 循环重试 (CAS Misses):
for (;;)
循环允许在 CAS 操作失败后进行重试。这是因为多个读线程可能同时尝试增加读锁计数,导致 CAS 竞争。 - 完整的重入处理: 它能正确处理所有线程的读锁重入,包括查找和更新
ThreadLocal
中的HoldCounter
。ReentrantReadWriteLock.java
// ...HoldCounter rh = null; // rh 会在需要时从 readHolds 或 cachedHoldCounter 获取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// 如果需要阻塞,但当前是重入读,则不应该阻塞。// 这里的逻辑是确保如果 readerShouldBlock() 为 true,// 并且当前不是重入(即 rh.count == 0),则获取失败。if (firstReader == current) {// assert firstReaderHoldCount > 0;} else {if (rh == null) { // 首次进入或 rh 未初始化rh = cachedHoldCounter;if (rh == null ||rh.tid != LockSupport.getThreadId(current)) {rh = readHolds.get(); // 从 ThreadLocal 获取if (rh.count == 0) // 如果 ThreadLocal 中计数为0,说明之前没有正确移除或状态异常readHolds.remove();}}if (rh.count == 0) // 如果当前线程的读锁计数为0,并且 readerShouldBlock() 为 true,则获取失败return -1L;}}if (sharedCount(c) == MAX_COUNT) // 读锁数量超限throw new Error("Maximum lock count exceeded");if (compareAndSetState(c, c + SHARED_UNIT)) { // 尝试CAS增加读锁计数// ... 成功获取后更新 firstReader/firstReaderHoldCount 或 rh.count ...return 1L; // 成功}}
- 处理锁降级: 在循环中,如果发现写锁被持有 (
exclusiveCount(c) != 0
),它会检查是否是当前线程持有。如果是,则允许继续尝试获取读锁(这是锁降级的关键)。 - 更细致的策略检查: 在循环的每次迭代中都会检查
readerShouldBlock()
。
readerShouldBlock
非公平情况下,会检查第一个是不是读锁。
公平情况下,会检查前面有没有头节点之外的节点,这意味着第二个读线程可以通过这个判断,然后第三个。。。。直到一个写节点加入。
writerShouldBlock
writerShouldBlock()
是 ReentrantReadWriteLock.Sync
类中的一个抽象方法,它在两个具体的子类 FairSync
和 NonfairSync
中有不同的实现。这个方法的核心作用是判断当前尝试获取写锁的线程是否应该因为特定的策略(公平性策略)而被阻塞,即使锁当前可能没有被其他线程持有。
来看一下它在 ReentrantReadWriteLock.Sync
类中的 tryAcquire
方法中是如何被使用的:
@Override@ReservedStackAccessprotected final boolean tryAcquire(long acquires) {// ... (省略了检查是否已有读锁或写锁被其他线程持有的逻辑) ...Thread current = Thread.currentThread();long c = getState();long w = exclusiveCount(c);if (c != 0) {// ... (处理已有锁的情况,包括重入) ...}// 当 c == 0 时,表示当前没有任何锁(既没有读锁也没有写锁)// 此时,即使锁是可用的,也需要根据策略判断是否应该阻塞if (writerShouldBlock() || // <--- 在这里被调用!compareAndSetState(c, c + acquires))return false; // 如果 writerShouldBlock() 返回 true,则获取失败,线程需要去排队setExclusiveOwnerThread(current);return true;}
当 tryAcquire
方法执行到 c == 0
的分支时,意味着当前锁是自由的(没有线程持有读锁或写锁)。在这种情况下:
-
调用
writerShouldBlock()
:- 这个方法的返回值决定了当前线程是否应该“礼让”其他可能在等待队列中的线程。
- 如果
writerShouldBlock()
返回true
,即使锁是可用的,tryAcquire
也会返回false
。这意味着当前线程获取写锁的尝试失败了,它将被 AQS 框架放入等待队列中。 - 如果
writerShouldBlock()
返回false
,则当前线程可以继续尝试通过compareAndSetState
(CAS) 来获取写锁。
-
compareAndSetState(c, c + acquires)
:- 如果
writerShouldBlock()
返回false
,则会尝试通过 CAS 操作来原子地更新锁的状态,从而获取写锁。 - 如果 CAS 失败(例如,在非公平模式下,另一个线程恰好在此时也尝试获取锁并成功了),
tryAcquire
同样返回false
。
- 如果
不同子类中的实现:
-
NonfairSync
(非公平同步器):ReentrantReadWriteLock.java
static final class NonfairSync extends Sync {// ...final boolean writerShouldBlock() {return false; // writers can always barge}// ... }
在非公平模式下,
writerShouldBlock()
总是返回false
。这意味着尝试获取写锁的线程总是可以“插队”或“闯入”(barge),它会直接尝试通过 CAS 获取锁,而不会检查等待队列中是否有其他线程。 -
FairSync
(公平同步器):static final class FairSync extends Sync {// ...final boolean writerShouldBlock() {return hasQueuedPredecessors();}// ... }
在公平模式下,
writerShouldBlock()
调用hasQueuedPredecessors()
。这个方法(继承自 AQS)会检查当前线程在同步队列中是否有前驱节点(即是否有其他线程比它更早开始等待获取锁)。- 如果
hasQueuedPredecessors()
返回true
,表示队列中有等待时间更长的线程,那么当前线程就应该阻塞,让等待时间长的线程先获取锁,以保证公平性。 - 如果返回
false
,表示当前线程是队列的头部或者队列为空,它可以尝试获取锁。
- 如果
持有计数优化
使用多级缓存机制优化读锁计数:
// 第一个读线程的快速路径
private transient Thread firstReader;
private transient int firstReaderHoldCount;// 最近访问线程的缓存
private transient HoldCounter cachedHoldCounter;// 线程本地存储
private transient ThreadLocalHoldCounter readHolds;
使用场景
适用于读多写少的并发场景,如:
- 缓存系统的读写操作
- 配置信息的访问和更新
- 统计数据的查询和修改
通过读写分离,ReentrantReadWriteLock
相比普通的互斥锁能显著提升读并发性能。