Java多线程中的读写锁(ReadWriteLock)
一、为什么需要读写锁?
普通的互斥锁(如 synchronized 或 ReentrantLock)无论读还是写都会加锁,导致读读之间也会互斥,性能低下。
但实际场景中,读操作远多于写操作,而且:
- 多个线程同时读不会产生数据竞争 → 应该允许并发
- 读和写、写和写之间才需要互斥
读写锁正是为此设计的:读读共享、读写互斥、写写互斥。
二、核心接口
java
public interface ReadWriteLock {
Lock readLock(); // 获取读锁
Lock writeLock(); // 获取写锁
}
主要实现类:ReentrantReadWriteLock
三、ReentrantReadWriteLock 的规则
| 组合 | 是否允许 |
|---|---|
| 读 + 读 | ✅ 允许(共享) |
| 读 + 写 | ❌ 互斥 |
| 写 + 写 | ❌ 互斥 |
| 写 + 读 | ❌ 互斥 |
关键特性:
- 可重入:读锁和写锁都支持重入。但同一线程持有写锁后可以再获取读锁(锁降级),反之不行(读锁不能升级为写锁,会死锁)。
- 公平性可选 :构造函数可传入
fair参数。- 非公平(默认):可能造成读线程饥饿,但吞吐量高。
- 公平:按队列顺序,避免饥饿,但性能略低。
java
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
ReentrantReadWriteLock fairLock = new ReentrantReadWriteLock(true);
四、基础使用示例
java
public class Cache {
private final Map<String, Object> map = new HashMap<>();
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
// 读操作
public Object get(String key) {
readLock.lock();
try {
return map.get(key);
} finally {
readLock.unlock();
}
}
// 写操作
public void put(String key, Object value) {
writeLock.lock();
try {
map.put(key, value);
} finally {
writeLock.unlock();
}
}
}
注意: 必须在 finally 中释放锁,避免异常导致锁泄漏。
五、锁降级(Lock Downgrade)
指持有写锁时获取读锁,然后释放写锁,从而让其他线程能读。这是允许的。
java
writeLock.lock();
try {
// 修改数据
map.put("k", "v");
readLock.lock(); // 获取读锁(降级)
} finally {
writeLock.unlock(); // 释放写锁
}
try {
// 此时持有读锁,其他读线程可并发
Object v = map.get("k");
} finally {
readLock.unlock();
}
为什么需要降级? 保证当前线程在修改后能立即读到自己的修改,同时不阻塞其他读线程,并防止其他写线程插入。
六、锁升级(不允许!)
java
readLock.lock();
// ... 读操作
writeLock.lock(); // ❌ 死锁!多个线程同时持有读锁时会互相等待
因为读锁是共享的,如果多个线程都持有读锁并试图升级为写锁,就会互相等待对方释放读锁,导致死锁。所以 ReentrantReadWriteLock 不支持锁升级。
七、内部原理简析
ReentrantReadWriteLock 内部基于 AQS(AbstractQueuedSynchronizer) 实现:
- state 高 16 位表示读锁持有数 ,低 16 位表示写锁持有数。
- 写锁:独占模式,类似
ReentrantLock。 - 读锁:共享模式,用
firstReader和cachedHoldCounter优化记录每个线程的读锁重入次数。
因为读写各有 16 位,所以最多支持 65535 个读锁或写锁重入次数。
八、适用场景与注意事项
适合:
- 读多写少的场景,如缓存、配置中心、字典查询。
- 需要保证数据一致性,但读操作并发度要求高。
不适合:
- 写操作非常频繁 → 读写锁的维护开销反而拖慢性能。
- 临界区非常短 → 直接用
synchronized或ReentrantLock可能更快。
注意事项:
- 读锁不支持
Condition,只有写锁可以newCondition()。 - 不要在持有读锁时调用
writeLock().lock()。 - 锁释放必须放在
finally。 - 非公平模式下,读线程可能长期获取不到锁(写线程排队时,新来的读线程会直接尝试抢锁)。
九、与 StampedLock 的简单对比(Java 8+)
StampedLock 是更快的读写锁,支持乐观读:
java
StampedLock sl = new StampedLock();
long stamp = sl.tryOptimisticRead(); // 乐观读,不加锁
// ... 读数据
if (!sl.validate(stamp)) { // 校验是否有写操作
stamp = sl.readLock(); // 退化为悲观读
try { /* 读 */ } finally { sl.unlockRead(stamp); }
}
StampedLock不可重入 ,不支持Condition,但性能更高。- 适合读多写少且对吞吐量要求极高的场景。
十、总结一句话
读写锁通过分离读锁和写锁,实现 读读并发、读写/写写互斥 ,在读多写少 的场景下显著提升并发性能;使用时要警惕锁升级死锁,并优先考虑是否需要
StampedLock这类更高效的替代方案。