Appearance
Locks-ReentrantReadWriteLock源码解析
一、RRWLock的设计目标
ReentrantLock
是排他锁(Exclusive Lock),同一时间只能有一个线程持有锁,适用于写多读少的场景。但在读多写少的场景(如缓存、数据库快照读),排他锁会导致大量读线程阻塞,降低并发性能。
RRWLock的设计目标是分离读写操作:
- 读锁(Shared Lock):多个读线程可同时持有,共享资源。
- 写锁(Exclusive Lock):同一时间只能有一个写线程持有,排他性。
通过读写分离,RRWLock在读多写少场景下的性能远优于ReentrantLock
。
二、RRWLock的核心特性
1. 读写互斥
- 写锁持有期间,所有读线程和其他写线程都无法获取锁;
- 读锁持有期间,所有写线程无法获取锁,但其他读线程可以继续获取读锁。
2. 可重入性
- 写线程:持有写锁的线程可以重入写锁(
writeLock.lock()
多次),也可以重入读锁(readLock.lock()
); - 读线程:持有读锁的线程可以重入读锁,但无法重入写锁(会导致死锁)。
3. 公平与非公平
- 非公平模式(默认):线程获取锁时不遵循等待顺序,允许“插队”(读线程可插队到写线程前),性能更高,但可能导致写线程饥饿(长期无法获取锁);
- 公平模式:线程按等待队列的顺序获取锁,避免饥饿,但性能稍差。
4. 锁降级
写线程可以降级为读线程(先获取写锁,再获取读锁,最后释放写锁),但读线程无法升级为写锁(会导致死锁)。
三、RRWLock的底层结构
RRWLock基于AQS(AbstractQueuedSynchronizer)实现,核心是Sync内部类(继承AQS),并分为 公平(FairSync) 和 非公平(NonfairSync) 两个子类。
1. 状态拆分(State的复用)
AQS的state
字段是32位int,RRWLock将其拆分为两部分:
- 高16位:读状态(
sharedCount
),表示当前持有读锁的线程数量(注意:不是线程数,而是读锁的重入次数总和); - 低16位:写状态(
exclusiveCount
),表示当前写锁的重入次数。
位运算定义(Sync
类中的常量):
java
private static final int SHARED_SHIFT = 16; // 读状态偏移量
private static final int SHARED_UNIT = (1 << SHARED_SHIFT); // 读状态单位(1<<16=65536)
private static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1; // 读写状态的最大值(65535)
private static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; // 写状态掩码(0xffff)
// 获取读状态(高16位)
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
// 获取写状态(低16位)
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
例如,state=0x00020003
:
- 读状态:
0x0002
(即2,代表读锁被重入2次); - 写状态:
0x0003
(即3,代表写锁被重入3次)。
2. 内部类结构
RRWLock有两个内部类实现Lock
接口:
ReadLock
:读锁,对应AQS的共享模式(tryAcquireShared
/tryReleaseShared
);WriteLock
:写锁,对应AQS的排他模式(tryAcquire
/tryRelease
)。
java
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
private final ReentrantReadWriteLock.ReadLock readerLock; // 读锁
private final ReentrantReadWriteLock.WriteLock writerLock; // 写锁
private final Sync sync; // 核心同步器
public ReentrantReadWriteLock() {
this(false); // 默认非公平模式
}
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
// 读锁内部类
public static class ReadLock implements Lock { /* ... */ }
// 写锁内部类
public static class WriteLock implements Lock { /* ... */ }
// 同步器抽象类(继承AQS)
abstract static class Sync extends AbstractQueuedSynchronizer { /* ... */ }
// 非公平同步器
static final class NonfairSync extends Sync { /* ... */ }
// 公平同步器
static final class FairSync extends Sync { /* ... */ }
}
四、源码深度剖析
1. 写锁(WriteLock)的实现
写锁是排他锁,其核心逻辑在Sync
的tryAcquire
(获取锁)和tryRelease
(释放锁)方法中。
(1)写锁获取:tryAcquire
tryAcquire
方法遵循以下逻辑:
- 若
state=0
(无锁),尝试CAS将state
设置为acquires
(默认1),成功则标记当前线程为排他所有者; - 若
state≠0
,检查是否是当前线程持有写锁(exclusiveCount(state)≠0
且getExclusiveOwnerThread()==current
),若是则重入(state+=acquires
); - 否则,返回
false
(获取失败,进入等待队列)。
非公平模式的tryAcquire
源码:
java
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c); // 获取写状态(低16位)
if (c != 0) {
// 情况1:state≠0(有锁持有)
// 子情况1.1:写状态为0(说明是读锁持有),或当前线程不是写锁所有者 → 失败
if (w == 0 || current != getExclusiveOwnerThread())
return false;
// 子情况1.2:写状态超过最大值(65535)→ 抛出错误
if (w + acquires > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 子情况1.3:重入写锁 → 更新state
setState(c + acquires);
return true;
}
// 情况2:state=0(无锁)→ 尝试CAS获取写锁
// writerShouldBlock():非公平模式返回false(允许插队),公平模式返回hasQueuedPredecessors()
if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
return false;
// 成功:标记当前线程为排他所有者
setExclusiveOwnerThread(current);
return true;
}
关键细节:
writerShouldBlock()
:非公平模式返回false
(允许写线程插队),公平模式返回hasQueuedPredecessors()
(检查等待队列是否有前驱节点,有则阻塞,遵循公平顺序);- 写锁的排他性:若有读锁持有(
w=0
但c≠0
),写线程无法获取锁,确保读写互斥。
(2)写锁释放:tryRelease
tryRelease
方法逻辑简单:
- 将
state
减去acquires
(默认1); - 若
state
减到0,标记排他所有者为null
(完全释放); - 返回
true
当且仅当state
减到0(表示写锁完全释放)。
源码:
java
protected final boolean tryRelease(int acquires) {
if (!isHeldExclusively()) // 当前线程不是写锁所有者 → 抛出异常
throw new IllegalMonitorStateException();
int nextc = getState() - acquires;
boolean free = exclusiveCount(nextc) == 0; // 写状态是否为0
if (free)
setExclusiveOwnerThread(null); // 完全释放,清除所有者
setState(nextc);
return free;
}
2. 读锁(ReadLock)的实现
读锁是共享锁,其核心逻辑在Sync
的tryAcquireShared
(获取锁)和tryReleaseShared
(释放锁)方法中。
(1)读锁获取:tryAcquireShared
读锁获取的逻辑更复杂,需要处理读共享、可重入和读写互斥:
- 若写锁被持有且不是当前线程(
exclusiveCount(c)≠0
且getExclusiveOwnerThread()≠current
),返回-1
(失败); - 若写锁未被持有或当前线程持有写锁(允许写线程重入读锁),尝试CAS增加读状态(
state+=SHARED_UNIT
); - 处理重入:记录当前线程的读锁重入次数(用
ThreadLocal
存储)。
源码:
java
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
// 情况1:写锁被持有且不是当前线程 → 失败(返回-1)
if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c); // 获取读状态(高16位)
// 情况2:尝试获取读锁(非阻塞且读状态未超最大值)
if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {
// 子情况2.1:第一次获取读锁(r=0)→ 记录firstReader(优化,避免ThreadLocal查找)
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
// 子情况2.2:firstReader重入 → 增加firstReaderHoldCount
firstReaderHoldCount++;
} else {
// 子情况2.3:其他线程重入 → 从ThreadLocal获取HoldCounter
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get(); // readHolds是ThreadLocal<HoldCounter>
else if (rh.count == 0)
readHolds.set(rh);
rh.count++; // 增加读锁次数
}
return 1; // 成功(返回正数)
}
// 情况3:CAS失败或需要阻塞 → 进入循环重试(fullTryAcquireShared)
return fullTryAcquireShared(current);
}
关键细节:
readerShouldBlock()
:非公平模式返回false
(允许读线程插队),公平模式返回hasQueuedPredecessors()
(遵循公平顺序);firstReader
优化:为了减少ThreadLocal
的查找开销,RRWLock用firstReader
记录第一个获取读锁的线程,其重入次数用firstReaderHoldCount
记录(避免每次都查ThreadLocal
);HoldCounter
:存储每个线程的读锁重入次数,用ThreadLocal<HoldCounter>
(readHolds
)实现:javastatic final class HoldCounter { int count = 0; final long tid = getThreadId(Thread.currentThread()); // 线程ID } private transient ThreadLocal<HoldCounter> readHolds = new ThreadLocal<>(); private transient HoldCounter cachedHoldCounter; // 缓存最近的HoldCounter,优化查找
(2)读锁释放:tryReleaseShared
读锁释放需要处理重入次数减少和读状态更新:
- 减少当前线程的读锁重入次数(更新
firstReaderHoldCount
或HoldCounter
); - 循环CAS减少读状态(
state-=SHARED_UNIT
); - 返回
true
当且仅当state
减到0(表示所有读锁都释放)。
源码:
java
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
// 情况1:当前线程是firstReader → 更新firstReaderHoldCount
if (firstReader == current) {
if (firstReaderHoldCount == 1)
firstReader = null; // 完全释放,清除firstReader
else
firstReaderHoldCount--;
} else {
// 情况2:其他线程 → 从ThreadLocal获取HoldCounter
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove(); // 完全释放,移除ThreadLocal中的记录
if (count <= 0)
throw new IllegalMonitorStateException(); // 释放次数超过获取次数
}
rh.count--; // 减少读锁次数
}
// 情况3:循环CAS更新读状态(避免并发修改)
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
return nextc == 0; // 返回true当且仅当所有锁都释放
}
}
3. 公平与非公平的区别
公平与非公平的核心区别在于**writerShouldBlock()
和readerShouldBlock()
**方法的实现:
- 非公平模式(NonfairSync):java
final boolean writerShouldBlock() { return false; // 写线程不阻塞,允许插队 } final boolean readerShouldBlock() { return false; // 读线程不阻塞,允许插队 }
- 公平模式(FairSync):java
final boolean writerShouldBlock() { return hasQueuedPredecessors(); // 写线程检查等待队列是否有前驱节点,有则阻塞 } final boolean readerShouldBlock() { return hasQueuedPredecessors(); // 读线程检查等待队列是否有前驱节点,有则阻塞 }
hasQueuedPredecessors()
:判断等待队列中是否有比当前线程更早等待的线程(即队列头部是否是当前线程),若有则返回true
(需要阻塞),否则返回false
(可以获取锁)。
五、关键细节与注意事项
1. 读锁无法升级为写锁
若一个线程持有读锁,再尝试获取写锁,会导致死锁:
java
ReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock readLock = rwLock.readLock();
Lock writeLock = rwLock.writeLock();
// 线程A的操作
readLock.lock();
try {
// 尝试获取写锁(读锁未释放)→ 阻塞
writeLock.lock();
// 永远无法执行到这里
} finally {
readLock.unlock();
}
原因:线程A持有读锁,尝试获取写锁时,需要等待所有读锁释放(包括自己的),但自己的读锁未释放,导致死锁。
2. 写锁可以降级为读锁
写线程可以安全地将写锁降级为读锁(先获取写锁,再获取读锁,最后释放写锁):
java
ReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock readLock = rwLock.readLock();
Lock writeLock = rwLock.writeLock();
// 线程A的操作
writeLock.lock();
try {
// 执行写操作(修改数据)
updateData();
// 获取读锁(降级)
readLock.lock();
} finally {
writeLock.unlock(); // 释放写锁,保留读锁
}
try {
// 执行读操作(读取修改后的数据)
readData();
} finally {
readLock.unlock(); // 释放读锁
}
好处:降级后,其他读线程可以共享数据,而当前线程仍持有读锁,确保数据一致性。
3. 写线程饥饿问题(非公平模式)
在非公平模式下,若有大量读线程持续获取读锁,写线程可能长期无法获取锁(饥饿)。例如:
- 写线程W在等待队列中;
- 读线程R1、R2、R3依次获取读锁,释放后,新的读线程R4又插队获取读锁;
- 写线程W一直无法获取锁。
解决方法:使用公平模式(ReentrantReadWriteLock(true)
),确保线程按等待顺序获取锁,避免饥饿。
4. 锁的中断性
RRWLock的读锁和写锁都支持中断(lockInterruptibly()
方法),即线程在等待锁时可以响应中断,抛出InterruptedException
。
六、使用场景
RRWLock适用于读多写少的场景,例如:
- 缓存系统:缓存更新(写锁)、缓存读取(读锁);
- 数据库快照读:事务中的快照读(读锁)、数据修改(写锁);
- 配置文件加载:配置文件更新(写锁)、配置读取(读锁)。
七、总结
ReentrantReadWriteLock
是读写分离的并发工具,核心思想是通过AQS的state字段拆分实现读写状态的管理,支持读共享、写排他,提高读多写少场景下的并发性能。其关键特性包括:
- 读写互斥:确保写操作的原子性;
- 可重入性:允许线程重入读写锁;
- 公平与非公平:平衡性能与饥饿问题;
- 锁降级:支持写锁降级为读锁,确保数据一致性。
在使用时,需要注意读锁无法升级为写锁、非公平模式下的写线程饥饿等问题,根据场景选择合适的公平策略。
参考资料:
- JDK 1.8源码(
java.util.concurrent.locks.ReentrantReadWriteLock
); - 《Java并发编程的艺术》(方腾飞等);
- 《深入理解Java虚拟机》(周志明)。