概述

ReadWriteLock(读写锁)适用于“读多写少”的共享数据场景。它把普通互斥锁拆成两种锁:
- 读锁:多个线程可以同时持有。
- 写锁:同一时间只能由一个线程持有,并且会排斥所有读锁。
这里的并发读成立于没有其他线程持有写锁,并且当前实现的排队策略允许获取读锁的前提下。
Java 中最常用的实现是 ReentrantReadWriteLock,内部提供两把逻辑锁:
Lock readLock();
Lock writeLock();
为什么需要读写锁
使用普通互斥锁时:
private final Lock lock = new ReentrantLock();
public String get(String key) {
lock.lock();
try {
return map.get(key);
} finally {
lock.unlock();
}
}
即使两个线程都只是读取数据,也不能同时执行:
线程 A 读取 ──────────────
线程 B 等待 ──── 读取
但如果读取操作不会修改共享数据,多个读线程通常可以安全并发:
线程 A 读取 ──────────────
线程 B 读取 ──────────────
线程 C 读取 ──────────────
只有写操作需要排他执行:
线程 A 读取 ─────
线程 B 读取 ─────
线程 C 写入 ─────
因此,读写锁把访问分成两种模式:
- 读锁:共享模式
- 写锁:独占模式
接口和实现
ReadWriteLock 接口
接口非常简单:它只负责提供两把关联的锁。
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
返回的是普通 Lock 接口,因此读锁和写锁都可以调用:
lock()
lockInterruptibly()
tryLock()
tryLock(timeout, unit)
unlock()
不过具体实现仍可限制某些操作。例如 ReentrantReadWriteLock 的写锁支持 newCondition(),读锁调用 newCondition() 则会抛出 UnsupportedOperationException。
ReentrantReadWriteLock
Java 最常用的读写锁实现:ReentrantReadWriteLock
- “Reentrant”表示它支持重入
ReentrantReadWriteLock rwLock =
new ReentrantReadWriteLock();
Lock readLock = rwLock.readLock();
Lock writeLock = rwLock.writeLock();
也可以配置公平策略:
new ReentrantReadWriteLock(true);
默认是非公平模式:
new ReentrantReadWriteLock(false);
锁降级
ReentrantReadWriteLock 支持从写锁降级为读锁。
所谓锁降级,是指:
持有写锁
↓
在释放写锁之前获得读锁
↓
释放写锁
↓
继续持有读锁
代码结构:
writeLock.lock();
try {
updateSharedState();
readLock.lock();
} finally {
writeLock.unlock();
}
try {
readSharedState();
} finally {
readLock.unlock();
}
锁降级的意义是:线程完成修改以后,仍然可以继续稳定地读取修改后的数据,同时允许其他读线程进入。
如果先释放写锁,再获取读锁:
writeLock.unlock();
readLock.lock();
两次操作之间存在空窗期,其他写线程可能抢先修改数据。
反方向的“锁升级”不受支持:线程持有读锁时再等待写锁不会成功,必须先释放读锁,再单独竞争写锁,并在获取后重新检查共享状态。
底层 state
ReentrantReadWriteLock 基于 Java AQS,但 AQS 只有一个:
volatile int state;
读写锁需要同时记录:
- 读锁总数量
- 写锁重入次数
因此,它把 32 位 int 拆成两部分:
高 16 位:读锁总计数
低 16 位:写锁重入次数
示意:
31 16 15 0
┌──────────────────────────┬──────────────────────────┐
│ 读锁计数 │ 写锁计数 │
└──────────────────────────┴──────────────────────────┘
获取读锁计数:
int readCount = state >>> 16;
获取写锁计数:
int writeCount = state & 0xFFFF;
两个计数各占 16 位,因此单个计数的最大值都是 65535;超过限制时实现会抛出错误,而不是继续溢出。