ReentrantReadWriteLock
是 Java 中 java.util.concurrent.locks
包提供的一个可重入的读写锁,它允许多个读线程同时访问共享资源,但在写线程存在时不允许任何读线程或写线程访问。这种机制非常适合读多写少
的场景。
一、基本概念
1. 什么是读写锁?
- 读锁(Read Lock):多个线程可以同时获取读锁(共享锁),适用于只读操作。
- 写锁(Write Lock):只有一个线程能获取写锁(排他锁),适用于修改数据的操作。
在并发编程中,如果多个线程都只是读取共享变量而不进行修改,则无需加锁互斥;只有在有线程要修改数据时才需要独占锁。
二、特性
-
锁分离机制
- 读锁:共享,不互斥(多个线程可同时持有)
- 写锁:独占,互斥(与其他读/写锁互斥)
-
锁降级
writeLock.lock(); // 1. 获取写锁 readLock.lock(); // 2. 获取读锁(锁降级开始) writeLock.unlock(); // 3. 释放写锁(降级完成) // 此时仍持有读锁
- 允许一个线程先持有写锁再获取读锁
- 禁止锁升级(读锁 → 写锁)可能引发死锁
-
公平性选择
- 公平模式:按等待队列顺序获取锁
- 非公平模式(默认):允许插队提高吞吐量
-
可重入性
- 同一线程可重复获取读锁/写锁
三、内部结构
ReentrantReadWriteLock
内部有两个子类:
ReentrantReadWriteLock.ReadLock
ReentrantReadWriteLock.WriteLock
通过一个 int
状态值(AQS 的 state
变量)同时维护两种锁:
- 高16位:读锁持有数量(包括重入次数)
- 低16位:写锁重入次数
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
// 读锁计数
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
// 写锁计数
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
四、示例
基础使用
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Cache {
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
private String data = "Initial Data"; // 模拟缓存数据
// 读取数据
public void readData() {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 正在读取数据: " + data);
Thread.sleep(500); // 模拟耗时操作
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
readLock.unlock();
}
}
// 更新数据
public void writeData(String newData) {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 正在写入数据: " + newData);
data = newData;
Thread.sleep(1000); // 模拟耗时写操作
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
writeLock.unlock();
}
}
public static void main(String[] args) {
Cache cache = new Cache();
// 创建多个读线程
for (int i = 0; i < 5; i++) {
new Thread(cache::readData, "Reader-" + i).start();
}
// 创建一个写线程
new Thread(() -> cache.writeData("New Updated Data"), "Writer").start();
}
}
说明:
- 所有
Reader
线程几乎同时执行读操作。 Writer
线程必须等待所有读线程释放锁后才能执行。- 写完成后,后续的读线程将读到新值。
缓存系统
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class CacheSystem<K, V> {
private final Map<K, V> cache = new HashMap<>();
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
// 读操作:使用读锁(共享)
public V get(K key) {
readLock.lock();
try {
return cache.get(key);
} finally {
readLock.unlock();
}
}
// 写操作:使用写锁(独占)
public void put(K key, V value) {
writeLock.lock();
try {
cache.put(key, value);
} finally {
writeLock.unlock();
}
}
// 锁降级:先更新再持续读取
public void updateAndRead(K key, V newValue) {
writeLock.lock();
try {
// 1. 更新数据(持有写锁)
cache.put(key, newValue);
// 2. 获取读锁(锁降级开始)
readLock.lock();
} finally {
writeLock.unlock(); // 3. 释放写锁(降级完成)
}
try {
// 4. 仍持有读锁,安全读取
System.out.println("Current value: " + cache.get(key));
} finally {
readLock.unlock();
}
}
}
五、锁降级
允许写锁降级为读锁,这在某些场景中非常有用,例如:
writeLock.lock();
try {
// 修改数据
doUpdate();
// 降级为读锁
readLock.lock();
} finally {
writeLock.unlock(); // 释放写锁,但保留读锁
}
try {
// 使用新数据做只读操作
useData();
} finally {
readLock.unlock();
}
注意:不能反过来,即不允许从读锁升级到写锁。
六、公平性设置
构造时可以传入布尔参数决定是否启用公平锁:
ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true); // true 表示公平模式
- 非公平模式(默认):吞吐量更高,但可能造成“饥饿”。
- 公平模式:线程按请求顺序获得锁,避免饥饿,但吞吐量较低。
七、常见问题
1. 读写锁和 synchronized 对比?
特性 | synchronized | ReentrantReadWriteLock |
---|---|---|
支持读写分离 | ❌ | ✅ |
支持尝试获取锁 | ❌ | ✅(tryLock) |
支持超时获取锁 | ❌ | ✅ |
支持中断响应 | ❌ | ✅ |
可重入 | ✅ | ✅ |
性能优化(读多写少) | ❌ | ✅ |
2. 为什么不能从读锁升级到写锁?
因为多个线程可能同时持有读锁,如果其中一个线程试图升级为写锁,就会破坏其他读线程的数据一致性。
八、适用场景
场景 | 是否推荐使用 ReentrantReadWriteLock |
---|---|
读多写少 | ✅ 强烈推荐 |
读写频率相当 | ⚠️ 视情况而定,注意锁竞争 |
写多读少 | ❌ 效率不高,建议使用普通锁 |
需要锁降级 | ✅ 优势明显 |
需要公平调度 | ✅ 可开启公平模式 |
九、建议
- 如果场景是读很多且写极少,可以考虑使用
StampedLock
,它是 JDK8 新增的更高效的读写锁实现。 - 如果使用的是 Spring 或者其它框架,也可以结合 AOP 来管理锁逻辑。
- 在分布式系统中,Java 原生锁不再适用,应使用分布式锁如 Redis、Zookeeper等。
十、注意事项
-
写锁饥饿问题
- 非公平模式下,大量读线程可能导致写线程长期等待
- 解决方案:使用公平锁或
StampedLock
-
锁升级禁止
// 错误示例(导致死锁): readLock.lock(); try { writeLock.lock(); // 阻塞等待其他读锁释放 } finally { readLock.unlock(); }
-
锁释放匹配
- 获取多少次锁就释放多少次
-
中断
- 写锁支持获取过程中响应中断
- 读锁不支持中断(
lockInterruptibly()
除外)