在Java并发包中,ReentrantReadWriteLock是常见的读写锁实现。它允许同一时刻多个读线程访问共享资源,但写线程必须独占访问。其“可重入”特性意味着同一线程可以多次获取已持有的锁而不会自己阻塞自己。理解它背后的持锁逻辑,对排查死锁、设计自定义同步器都很关键。

一、同步状态的高低位拆分
ReentrantReadWriteLock内部基于AbstractQueuedSynchronizer(AQS)实现,但并没有直接使用单一的state整数来表示锁状态,而是把一个int类型的state拆成了两部分:高16位表示读锁的持有数量,低16位表示写锁的持有数量。这种设计让读写状态可以共存于同一个原子变量中,减少了额外的字段开销。
具体来说,写锁每次获取成功,低16位加一;释放时低16位减一。读锁则稍复杂,高16位记录所有线程持有的读锁总数,而每个线程自身的重入次数保存在ThreadLocal中(具体为HoldCounter结构)。这样即使多个读线程同时重入,也不会互相覆盖计数。
// 状态拆分示意(来自ReentrantReadWriteLock源码逻辑)
static final int SHARED_SHIFT = 16;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; // 0xFFFF
static int sharedCount(int c) { return c >>> SHARED_SHIFT; } // 读锁数
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; } // 写锁数
二、写锁的持锁与重入逻辑
写锁是独占锁。当一个线程调用writeLock().lock()时,AQS会进入tryAcquire方法。该方法先读取当前state,若读锁计数不为0,说明有线程正在读,写线程必须等待;若写锁计数不为0,再判断当前线程是否为持锁线程,若是则重入,低16位加一并返回成功。
如果当前没有写锁也没有读锁,或者当前线程就是持有写锁的线程,那么通过CAS设置state的低16位。重入次数理论上受限于低16位的最大值(65535),超出会抛出Error。释放写锁时,tryRelease将低16位减一,直到为0才彻底释放,此时会唤醒后续等待节点。
// 写锁重入获取的核心片段(简化)
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
// 有读锁或其他写线程持锁则失败
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > 65535)
throw new Error("Maximum lock count exceeded");
}
if (!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
三、读锁的持锁与重入逻辑
读锁是共享锁。线程调用readLock().lock()时进入tryAcquireShared。若写锁被其他线程持有,则获取失败;若当前线程持有写锁,也可以获取读锁,这就是“锁降级”的基础。读锁重入时,利用ThreadLocalHoldCounter记录每个线程的重入次数,同时高16位总读计数加一。
释放读锁时,先减少ThreadLocal中的重入计数,再减少高16位总读计数。只有当总读计数为0时,才完全释放读锁。由于读锁是共享模式,AQS会以传播方式唤醒后续连续的读节点,提升并发吞吐。
// 读锁重入获取简化逻辑
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1; // 其他写线程独占,失败
int r = sharedCount(c);
if (r < 65535 && compareAndSetState(c, c + (1 << 16))) {
// 首次获取读锁,记录HoldCounter
return 1;
}
return fullTryAcquireShared(current); // 处理重入与CAS重试
}
四、锁降级与常见误区
ReentrantReadWriteLock支持锁降级:持有写锁的线程可以获取读锁,然后释放写锁,从而把独占访问降级为共享访问。这能防止在修改完数据后、准备读取结果时,被其他写线程插空修改。但需要注意的是,锁升级(先读后写)是不支持的,如果线程先拿读锁再尝试拿写锁,会直接死锁。
一个常见误区是认为读锁和写锁完全独立。实际上,由于state共用,写锁重入时若中途获取读锁,读锁计数也会变化;释放顺序错误可能导致写锁提前释放。因此在编码时应明确“谁持有、谁释放、按相反顺序释放”的原则。
// 正确的锁降级示例
ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
rw.writeLock().lock();
try {
// 修改数据
rw.readLock().lock(); // 降级:获取读锁
} finally {
rw.writeLock().unlock(); // 释放写锁,仍持有读锁
}
// 后续可读数据
rw.readLock().unlock();
五、自定义可重入读写锁的要点
如果你想基于AQS自己实现一个类似的可重入读写锁,核心就是复用“高低位拆分state”的思路,并分别为独占模式与共享模式实现tryAcquire/tryRelease和tryAcquireShared/tryReleaseShared。重入判断都依赖“当前线程是否等于owner线程”或“ThreadLocal计数是否大于0”。
此外,要避免在写锁临界区内调用可能再获取写锁的外部方法,否则虽不会死锁但会增加重入层数,难以维护。读锁重入则需注意ThreadLocal的内存泄漏问题,应在释放时清理计数器。理解ReentrantReadWriteLock的持锁逻辑,能让你在并发设计中少走很多弯路。
| 锁类型 | 模式 | 重入记录位置 | 释放条件 |
|---|---|---|---|
| 写锁 | 独占 | state低16位+owner线程 | 低16位归零 |
| 读锁 | 共享 | state高16位+ThreadLocal | 高16位归零 |
ReentrantReadWriteLock可重入锁读写锁修改时间:2026-08-07 20:57:32