在Java并发编程中,锁是解决线程安全问题的核心工具,而 ReentrantReadWriteLock(可重入读写锁) 作为Lock接口的重要实现,凭借“读写分离”的特性,在高并发读多写少场景中性能远超synchronized和ReentrantLock(排他锁)。
它既能保证多线程读操作的并发安全性,又能兼顾写操作的原子性,同时支持重入特性,极大提升了并发程序的灵活性和性能。
一、ReentrantReadWriteLock 核心特性
在深入源码前,我们先明确ReentrantReadWriteLock的核心特性,这是理解源码设计的基础,也是它区别于其他锁的关键:
-
读写分离:维护两把锁——读锁(共享锁)和写锁(排他锁)。多线程可同时获取读锁(并发读),但同一时刻只能有一个线程获取写锁(排他写);写锁持有期间,所有读线程和其他写线程均会被阻塞。
-
可重入性:读锁和写锁均支持重入。读线程获取读锁后,可再次获取读锁;写线程获取写锁后,可再次获取写锁,也可获取读锁(写锁降级为读锁)。
-
公平/非公平模式:支持两种锁获取模式,默认是非公平模式(性能更优),可通过构造函数指定为公平模式。公平模式下,线程按等待队列顺序获取锁;非公平模式下,线程可能插队获取锁。
-
锁降级:支持写锁降级为读锁(写线程持有写锁时,可获取读锁,再释放写锁,最终持有读锁),但不支持读锁升级为写锁(避免死锁)。
-
可中断性:支持锁获取的中断(
lockInterruptibly()方法),线程在等待锁的过程中可被中断,避免无限等待。
补充:ReentrantReadWriteLock实现了 ReadWriteLock 接口,该接口仅定义了两个核心方法:readLock() 获取读锁,writeLock() 获取写锁,所有核心逻辑均在 ReentrantReadWriteLock 的内部类中实现。
二、源码解析
ReentrantReadWriteLock 的源码结构清晰,核心由三个部分组成:
ReadWriteLock接口实现- 内部同步器
Sync(继承AQS)的实现。 - 读锁(
ReadLock)和写锁(WriteLock)的实现。
其中,Sync 是整个锁机制的核心,读写锁的获取、释放、重入等逻辑均依赖于 Sync 类,而 Sync 又分为公平模式(FairSync)和非公平模式(NonfairSync)两个子类。
整体类结构(简化)
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
// 读锁实例(内部类,实现Lock接口)
private final ReentrantReadWriteLock.ReadLock readerLock;
// 写锁实例(内部类,实现Lock接口)
private final ReentrantReadWriteLock.WriteLock writerLock;
// 核心同步器(继承AQS,实现锁的核心逻辑)
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 ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
// 获取写锁
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
// 核心同步器(继承AbstractQueuedSynchronizer)
abstract static class Sync extends AbstractQueuedSynchronizer { ... }
// 非公平模式同步器
static final class NonfairSync extends Sync { ... }
// 公平模式同步器
static final class FairSync extends Sync { ... }
// 读锁实现(实现Lock接口)
public static class ReadLock implements Lock { ... }
// 写锁实现(实现Lock接口)
public static class WriteLock implements Lock { ... }
}
核心:AQS状态的复用设计(关键难点)
AQS(AbstractQueuedSynchronizer)的核心是一个volatile修饰的int状态变量(state),用于表示锁的持有状态。对于ReentrantLock(排他锁),state=0表示无锁,state>0表示有线程持有锁(state的值即重入次数)。
但ReentrantReadWriteLock需要维护读锁和写锁两种状态,而AQS只有一个state变量,因此它采用了状态复用的设计:将int类型的state(32位)拆分为两部分,高16位表示读锁的持有次数(共享锁计数),低16位表示写锁的持有次数(排他锁计数)。
abstract static class Sync extends AbstractQueuedSynchronizer {
// 1. 写锁状态的掩码(低16位):用于提取写锁状态
static final int EXCLUSIVE_MASK = (1 << 16) - 1; // 0x0000FFFF
// 2. 读锁状态的移位位数(高16位):用于将读锁计数移到高16位
static final int SHARED_SHIFT = 16;
// 3. 读锁每次重入,状态增加的值(1 << 16)
static final int SHARED_UNIT = (1 << SHARED_SHIFT); // 0x00010000
// 4. 读锁最大持有次数(高16位最大为0xFFFF,即65535)
static final int MAX_COUNT = (1 << 16) - 1; // 0x0000FFFF
// 5. 读锁最大重入次数(同MAX_COUNT)
static final int MAX_SHARED_COUNT = MAX_COUNT;
// 提取写锁状态(低16位)
int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
// 提取读锁状态(高16位,右移16位)
int sharedCount(int c) { return c >>> SHARED_SHIFT; }
}
写锁(排他锁)源码解析:获取与释放
写锁是排他锁,实现类为ReentrantReadWriteLock.WriteLock,同样实现了Lock接口,核心方法委托给Sync同步器实现。写锁的获取逻辑核心是**“判断读锁或写锁是否被其他线程持有,若未被持有,则获取写锁;若被持有,则阻塞”**,同时支持重入、锁降级和公平/非公平模式。
写锁的获取(lock()方法)
WriteLock的lock()方法本质是调用Sync的acquire(1)方法,而acquire()是AQS的排他锁获取模板方法,核心逻辑由Sync的tryAcquire()方法实现。
// WriteLock的lock()方法
public void lock() {
sync.acquire(1);
}
// AQS的acquire()模板方法(排他锁获取)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt(); // 若获取失败,进入等待队列阻塞,支持中断
}
// Sync的tryAcquire()方法(核心:写锁获取逻辑)
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
// 1. 写锁当前持有次数(低16位)
int w = exclusiveCount(c);
if (c != 0) { // 状态不为0,说明读锁或写锁被持有
// ① 写锁持有次数为0(说明读锁被持有),或 ② 持有锁的线程不是当前线程 → 写锁获取失败
if (w == 0 || current != getExclusiveOwnerThread())
return false;
// 2. 检查写锁重入次数是否超过最大值(65535)
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 3. 写锁重入:CAS修改state(累加写锁计数)
setState(c + acquires);
return true;
}
// 4. 状态为0(无锁):尝试获取写锁(公平/非公平判断)
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
// 5. 获取写锁成功,设置当前线程为写锁持有者
setExclusiveOwnerThread(current);
return true;
}
写锁获取的核心逻辑拆解
-
锁状态判断:
若state != 0,说明读锁或写锁被持有:-
若
w == 0(写锁未被持有),则说明读锁被持有(state高16位>0),此时写锁获取失败(读锁存在时,不能获取写锁,避免读脏数据)。 -
若持有锁的线程不是当前线程,写锁获取失败(排他锁特性,只能有一个线程持有)。
-
-
重入校验:若当前线程持有写锁,则检查重入次数是否超过最大值(65535),若未超过,CAS修改
state(累加写锁计数),重入成功。 -
无锁状态获取:若
state == 0(无锁),则判断是否需要排队(writerShouldBlock()):-
非公平模式:
writerShouldBlock()返回false,直接CAS修改state,获取写锁。 -
公平模式:
writerShouldBlock()返回true(判断等待队列是否有线程排队,若有则排队),此时不能插队。
-
-
锁持有者设置:获取写锁成功后,调用
setExclusiveOwnerThread(current),将当前线程设为写锁持有者(AQS中的方法,用于标记排他锁的持有者)。
写锁的释放(unlock()方法)
写锁的释放逻辑核心是“递减写锁计数,若计数减为0,则释放写锁(清空锁持有者)”,释放后唤醒等待队列中的后续线程。
// WriteLock的unlock()方法
public void unlock() {
sync.release(1);
}
// AQS的release()模板方法(排他锁释放)
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 释放成功,唤醒等待队列中的后续线程
return true;
}
return false;
}
// Sync的tryRelease()方法(核心:写锁释放逻辑)
protected final boolean tryRelease(int releases) {
// 1. 检查当前线程是否持有写锁(否则抛出异常)
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 2. 写锁计数递减(低16位)
int nextc = getState() - releases;
// 3. 判断写锁是否完全释放(计数减为0)
boolean free = exclusiveCount(nextc) == 0;
if (free) {
setExclusiveOwnerThread(null); // 清空写锁持有者
}
// 4. 更新state(即使写锁未完全释放,也需更新状态,用于重入计数)
setState(nextc);
return free;
}
写锁释放的关键:只有当写锁的重入次数减为0时,才会清空锁持有者(setExclusiveOwnerThread(null)),并唤醒等待队列中的后续线程;若重入次数未减为0,则仅更新state,写锁仍被当前线程持有。
锁降级(写锁→读锁)
ReentrantReadWriteLock支持锁降级,即写线程持有写锁时,可获取读锁,再释放写锁,最终持有读锁。
锁降级的核心目的是“避免写锁释放后,其他线程修改数据,导致当前线程读取到脏数据”。
// 1. 获取写锁
writeLock.lock();
try {
// 2. 执行业务逻辑(写操作)
updateData();
// 3. 获取读锁(写锁持有期间,可直接获取读锁,无需阻塞)
readLock.lock();
} finally {
// 4. 释放写锁(此时持有读锁,实现锁降级)
writeLock.unlock();
}
try {
// 5. 执行读操作(持有读锁,可并发读,且数据不会被修改)
readData();
} finally {
// 6. 释放读锁
readLock.unlock();
}
注意:不支持读锁升级为写锁(读锁持有期间,无法获取写锁)。因为若多个线程同时持有读锁,均尝试升级为写锁,会导致死锁(每个线程都持有读锁,等待其他线程释放读锁,无法获取写锁)。
读锁(共享锁)源码解析:获取与释放
读锁是共享锁,实现类为ReentrantReadWriteLock.ReadLock,它实现了Lock接口,核心方法(lock()、unlock())均委托给Sync同步器实现。
读锁的获取逻辑核心是“判断写锁是否被持有,若未被持有,则累加读锁计数;若被持有,则阻塞”,同时支持重入和公平/非公平模式。
读锁的获取(lock()方法)
ReadLock的lock()方法本质是调用Sync的acquireShared(1)方法,而acquireShared()是AQS的共享锁获取模板方法,核心逻辑由Sync的tryAcquireShared()方法实现(模板方法模式)。
// ReadLock的lock()方法
public void lock() {
sync.acquireShared(1);
}
// AQS的acquireShared()模板方法(共享锁获取)
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg); // 若获取失败,进入等待队列阻塞
}
// Sync的tryAcquireShared()方法(核心:读锁获取逻辑)
protected final int tryAcquireShared(int unused) {
// 获取当前线程
Thread current = Thread.currentThread();
// 获取当前AQS状态
int c = getState();
// 1. 判断写锁是否被持有(exclusiveCount(c) != 0),且持有写锁的线程不是当前线程
// 若写锁被其他线程持有,读锁获取失败,返回-1(进入阻塞)
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// 2. 读锁计数(高16位)
int r = sharedCount(c);
// 3. 非公平模式下:判断是否需要阻塞(读者是否需要排队)、读锁计数是否超过最大值
// 公平模式下,需先判断等待队列是否有线程排队(hasQueuedPredecessors())
if (!readerShouldBlock() &&
r < MAX_SHARED_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
// 4. 读锁获取成功:处理重入和首次获取的逻辑
if (r == 0) { // 首次获取读锁(读锁计数从0变为1)
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) { // 当前线程是首次获取读锁的线程(重入)
firstReaderHoldCount++;
} else { // 其他线程重入读锁,用ThreadLocal维护每个线程的读锁持有次数
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1; // 读锁获取成功,返回1
}
// 5. 若上述条件不满足(如读锁计数超界、需要排队),进入自旋重试获取读锁
return fullTryAcquireShared(current);
}
读锁获取的核心逻辑拆解
- 写锁判断:首先判断写锁是否被持有(
exclusiveCount( c ) != 0),若被持有,且持有写锁的不是当前线程,则读锁获取失败(返回-1),进入等待队列阻塞;若持有写锁的是当前线程,则允许获取读锁(支持写锁降级)。 - 读锁计数校验:读取当前读锁计数(
r = sharedCount(c)),判断读锁计数是否超过最大值(65535),若超过则抛出异常(fullTryAcquireShared()中处理)。 - 公平/非公平判断:
- 非公平模式(默认):
readerShouldBlock()返回false,无需排队,直接尝试CAS修改state(累加读锁计数)。 - 公平模式:
readerShouldBlock()返回true(判断等待队列是否有线程排队,若有则需要排队),此时不能插队,需进入等待队列。
- 非公平模式(默认):
- 重入处理:用两种方式维护读锁的重入次数:
- 首次获取读锁的线程(
firstReader):用变量firstReaderHoldCount记录重入次数(优化性能,避免频繁操作ThreadLocal)。 - 其他线程:用
ThreadLocal<HoldCounter>(readHolds)维护每个线程的读锁重入次数,HoldCounter是一个内部类,存储线程ID和重入次数。
- 首次获取读锁的线程(
- 自旋重试:若CAS修改
state失败(如并发修改冲突)、读锁计数超界等,进入fullTryAcquireShared()方法,通过自旋重试获取读锁,避免直接阻塞,提升性能。
读锁的释放(unlock()方法)
读锁的释放逻辑核心是“递减读锁计数,若计数减为0,则读锁完全释放”,同时维护重入次数的递减,释放后唤醒等待队列中的后续线程。
// ReadLock的unlock()方法
public void unlock() {
sync.releaseShared(1);
}
// AQS的releaseShared()模板方法(共享锁释放)
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared(); // 释放成功,唤醒等待队列中的后续线程
return true;
}
return false;
}
// Sync的tryReleaseShared()方法(核心:读锁释放逻辑)
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
// 1. 判断当前线程是否是首次获取读锁的线程
if (firstReader == current) {
// 重入次数减1,若减为0,则清空firstReader
if (firstReaderHoldCount == 1)
firstReader = null;
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(); // 重入次数为0,移除ThreadLocal中的记录
if (count < 0)
throw unmatchedUnlockException();
}
rh.count = count - 1;
}
// 3. CAS递减读锁计数(高16位)
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// 读锁计数减为0时,返回true(唤醒后续线程);否则返回false
return nextc == 0;
}
}
读锁释放的关键:读锁是共享锁,多个线程持有读锁时,只有最后一个释放读锁的线程(读锁计数减为0),才会唤醒等待队列中的后续线程(可能是读线程或写线程),避免频繁唤醒,提升性能。
重入机制与公平/非公平模式补充
重入机制的底层实现
ReentrantReadWriteLock的重入机制,本质是通过“状态计数”和“线程持有标记”实现的:
-
写锁重入:通过
state的低16位(写锁计数)记录重入次数,当前线程持有写锁时,再次获取写锁,只需累加写锁计数(CAS修改state`)。 -
读锁重入:通过
firstReaderHoldCount(首次读线程)和ThreadLocal<HoldCounter>(其他线程)记录每个线程的读锁重入次数,再次获取读锁时,累加对应计数,同时累加state的高16位(读锁计数)。
公平与非公平模式的区别
公平与非公平模式的核心区别的是“线程获取锁时,是否允许插队”,由readerShouldBlock()和writerShouldBlock()两个方法控制(Sync的抽象方法,FairSync和NonfairSync分别实现)。
// 非公平模式(NonfairSync)
static final class NonfairSync extends Sync {
@Override
final boolean writerShouldBlock() {
return false; // 写锁无需排队,允许插队
}
@Override
final boolean readerShouldBlock() {
// 读锁仅在等待队列头部是写线程时,才需要排队(避免写线程饥饿)
return apparentlyFirstQueuedIsExclusive();
}
}
// 公平模式(FairSync)
static final class FairSync extends Sync {
@Override
final boolean writerShouldBlock() {
// 写锁需要排队(判断等待队列是否有线程排队)
return hasQueuedPredecessors();
}
@Override
final boolean readerShouldBlock() {
// 读锁需要排队(判断等待队列是否有线程排队)
return hasQueuedPredecessors();
}
}
补充说明:
- 非公平模式(默认):性能更优,写锁和读锁均可插队获取锁,但可能导致线程饥饿(某些线程长期无法获取锁)。
- 公平模式:线程按等待队列顺序获取锁,避免线程饥饿,但性能略差(频繁切换线程上下文)。
实战场景
适用场景
ReentrantReadWriteLock 最适合读多写少的高并发场景,例如:
-
缓存系统:缓存查询(读)频繁,缓存更新(写)稀少。
-
配置中心:配置读取(读)频繁,配置修改(写)稀少。
-
日志系统:日志查询(读)频繁,日志写入(写)相对稀少。
注意:若写操作频繁(读少写多),不建议使用ReentrantReadWriteLock,因为写锁的排他性会导致读线程频繁阻塞,性能不如ReentrantLock(排他锁)。
常见坑点
-
读锁升级为写锁:严禁尝试读锁升级为写锁,会导致死锁。
-
锁释放顺序:重入场景下,读锁/写锁的释放次数必须与获取次数一致,否则会导致锁泄漏(锁无法完全释放)。
-
写锁饥饿:非公平模式下,若读操作过于频繁,写线程可能长期无法获取写锁(饥饿),可通过切换为公平模式解决,但需牺牲部分性能。
-
内存可见性:读锁和写锁均保证内存可见性(
volatile state的happens-before关系),无需额外添加volatile修饰共享变量。
总结
ReentrantReadWriteLock 是Java并发编程中极具实用性的锁机制,其核心设计是“读写分离”和“状态复用”:通过拆分32位state变量,同时维护读锁(高16位)和写锁(低16位)的状态,实现了多线程并发读、单线程排他写的特性,同时支持重入、锁降级、公平/非公平模式,兼顾了并发安全性和性能。
核心源码逻辑回顾:
-
读锁(共享锁):判断写锁是否被其他线程持有,若未被持有,累加读锁计数,支持重入;释放时递减计数,最后一个释放者唤醒后续线程。
-
写锁(排他锁):判断读锁或写锁是否被其他线程持有,若未被持有,获取写锁,支持重入;释放时递减计数,计数为0时释放锁并唤醒后续线程。
-
重入机制:通过
state计数和ThreadLocal(读锁)、锁持有者标记(写锁)实现。 -
公平/非公平模式:通过
readerShouldBlock()和writerShouldBlock()控制线程是否排队。
实际开发中,需根据场景选择是否使用ReentrantReadWriteLock——读多写少场景优先使用,可显著提升并发性能;读少写多场景建议使用ReentrantLock,避免写线程饥饿。掌握其底层原理,能帮助我们更合理地使用锁,写出高效、安全的并发程序。
源码解析&spm=1001.2101.3001.5002&articleId=157842185&d=1&t=3&u=eafc68297d584d70809d4f5d157f5c88)
2440

被折叠的 条评论
为什么被折叠?



