ReentrantReadWriteLock是Java中基于AQS的读写分离锁,支持可重入、公平模式及锁降级,通过读写分离机制优化读多写少场景的并发性能,需避免锁升级引发死锁。
一、核心特性
ReentrantReadWriteLock 是 Java 并发包中基于 ReadWriteLock 接口的实现类,提供 读写分离的锁机制,适用于 读多写少 的场景。其核心特性如下:
| 特性 | 说明 |
|---|---|
| 读写分离 | 允许多个线程同时持有读锁(共享锁),但写锁(独占锁)只能被一个线程持有。 |
| 可重入性 | 读锁和写锁均支持重入(同一线程可重复获取锁)。 |
| 公平性选择 | 支持公平模式(按请求顺序分配锁)和非公平模式(允许插队)。 |
| 锁降级 | 允许线程在持有写锁时获取读锁,再释放写锁,实现从写锁到读锁的降级。 |
| 锁升级限制 | 不支持锁升级(持有读锁时不能直接获取写锁,需先释放读锁)。 |
二、实现原理
ReentrantReadWriteLock 基于 AbstractQueuedSynchronizer (AQS) 实现,通过一个 state 变量(int 类型)的高 16 位和低 16 位分别管理读锁和写锁的状态。
-
状态拆分
- 高 16 位:记录读锁的持有次数(包括重入次数)。
- 低 16 位:记录写锁的持有次数(写锁是独占的)。
-
读锁(ReadLock)
-
共享模式:多个线程可同时获取读锁。
-
实现逻辑:
protected int tryAcquireShared(int unused) { // 检查是否有写锁被持有(非当前线程) if (exclusiveCount(getState()) != 0 && getExclusiveOwnerThread() != currentThread) return -1; // 获取失败 // 通过 CAS 增加读锁计数 // ... }
-
-
写锁(WriteLock)
-
独占模式:同一时间只能有一个线程持有写锁。
-
实现逻辑:
protected boolean tryAcquire(int unused) { // 检查是否有其他线程持有读锁或写锁 if (getState() != 0) { if (currentThread != getExclusiveOwnerThread()) return false; // 获取失败 } // 通过 CAS 设置写锁状态 // ... }
-
-
锁降级(Write → Read Lock)
-
步骤:
- 获取写锁。
- 修改共享数据。
- 获取读锁(防止其他写线程修改数据)。
- 释放写锁。
- 后续操作使用读锁保证数据可见性。
-
示例:
writeLock.lock(); try { // 修改数据 readLock.lock(); // 锁降级 } finally { writeLock.unlock(); } try { // 读取数据(其他写线程无法修改) } finally { readLock.unlock(); }
-
三、使用场景
| 场景 | 说明 |
|---|---|
| 缓存系统 | 读操作频繁,写操作较少(如缓存数据的加载和更新)。 |
| 配置管理 | 多线程读取配置,偶尔更新配置(需保证更新时的独占性)。 |
| 数据统计 | 高频读取统计数据,低频更新统计结果。 |
| 数据库连接池 | 管理连接时,允许多线程获取连接(读),但扩容或缩容时需独占锁(写)。 |
四、性能与注意事项
-
适用场景限制
- 读多写少:读写锁在写操作频繁时性能可能不如普通互斥锁(如
ReentrantLock)。 - 线程竞争激烈:非公平模式在高并发场景下可能导致线程饥饿。
- 读多写少:读写锁在写操作频繁时性能可能不如普通互斥锁(如
-
避免锁升级
-
持有读锁时尝试获取写锁会导致死锁,必须按顺序释放读锁后再获取写锁:
readLock.lock(); try { // 读操作 readLock.unlock(); // 必须先释放读锁 writeLock.lock(); // 再获取写锁 // 写操作 } finally { writeLock.unlock(); }
-
-
公平模式 vs 非公平模式
- 公平模式:按请求顺序分配锁,避免饥饿,但吞吐量低。
- 非公平模式:允许插队,吞吐量高,但可能导致某些线程长期等待。
五、代码示例(缓存系统)
public class Cache<K, V> {
private final Map<K, V> map = new HashMap<>();
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
// 读缓存
public V get(K key) {
readLock.lock();
try {
return map.get(key);
} finally {
readLock.unlock();
}
}
// 写缓存
public void put(K key, V value) {
writeLock.lock();
try {
map.put(key, value);
} finally {
writeLock.unlock();
}
}
// 锁降级示例:更新并读取数据
public void updateAndRead(K key, V value) {
writeLock.lock();
try {
// 修改数据
map.put(key, value);
// 锁降级
readLock.lock();
} finally {
writeLock.unlock();
}
try {
// 读取数据(其他线程无法修改)
System.out.println(map.get(key));
} finally {
readLock.unlock();
}
}
}
六、对比其他锁机制
| 锁类型 | 优势 | 劣势 |
|---|---|---|
| synchronized | 语法简单,自动释放锁。 | 无法实现读写分离,性能较低。 |
| ReentrantLock | 支持公平锁、可中断锁。 | 读写操作均需互斥,不适合读多写少场景。 |
| ReentrantReadWriteLock | 读写分离,提高读性能。 | 写锁饥饿问题(公平模式下可缓解)。 |
总结
ReentrantReadWriteLock 通过读写分离显著优化了读多写少场景的性能,但其正确使用需注意以下要点:
- 锁降级是安全操作,但锁升级必须避免。
- 在写操作频繁的场景下,优先考虑普通互斥锁。
- 合理选择公平模式与非公平模式,平衡吞吐量与公平性。
通过合理应用读写锁,可以在保证线程安全的同时,最大化系统的并发处理能力。