ReadWriteLock详解
1. 基本概念
ReadWriteLock(读写锁)是 Java 并发包中提供的一种高级锁机制,它允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。这种机制在读多写少的场景下能够显著提高系统的并发性能。
1.1 主要特点
- 允许多个线程同时获取读锁
- 只允许一个线程获取写锁
- 写锁会阻塞其他读锁和写锁
- 读锁会阻塞写锁,但不会阻塞其他读锁
2. 实现原理
2.1 核心组件
- ReentrantReadWriteLock:ReadWriteLock 接口的主要实现类
- ReadLock:读锁实现
- WriteLock:写锁实现
- Sync:同步器实现(继承自 AQS)
2.2 锁状态设计
ReentrantReadWriteLock 使用一个 32 位的 int 变量来表示锁的状态:
- 高 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;
3. 读写分离控制机制
3.1 状态变量设计
ReentrantReadWriteLock 通过巧妙的状态变量设计来实现读写分离:
// 完整的状态变量结构
abstract static class Sync extends AbstractQueuedSynchronizer {
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;
// 读锁计数,存储在高16位
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
// 写锁计数,存储在低16位
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
}
3.2 读写状态管理
- 写锁状态管理:
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) { // 当前已有锁
if (w == 0 || current != getExclusiveOwnerThread())
return false; // 存在读锁或者写锁被其他线程持有
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
setState(c + acquires);
return true;
}
if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
- 读锁状态管理:
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1; // 存在写锁且不是当前线程持有
int r = sharedCount(c);
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
// 首次获取读锁或增加读锁计数
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
// 维护每个线程的读锁计数
}
return 1;
}
// 处理其他获取读锁的场景
return fullTryAcquireShared(current);
}
3.3 读写互斥控制
-
写锁获取时的互斥控制:
- 检查是否存在读锁(c != 0 && w == 0)
- 检查是否存在其他线程的写锁(current != getExclusiveOwnerThread())
- 使用 CAS 操作保证原子性
-
读锁获取时的互斥控制:
- 检查是否存在写锁(exclusiveCount(c) != 0)
- 检查写锁是否被当前线程持有(支持锁降级)
- 使用 CAS 操作保证计数更新的原子性
3.4 锁降级流程
public void processData() {
readLock.lock(); // 1. 获取读锁
if (!update) {
readLock.unlock(); // 2. 释放读锁
writeLock.lock(); // 3. 获取写锁
try {
if (!update) {
// 更新数据
update = true;
}
readLock.lock(); // 4. 获取读锁
} finally {
writeLock.unlock(); // 5. 释放写锁
}
// 现在仍然持有读锁
}
try {
// 使用数据
} finally {
readLock.unlock(); // 6. 释放读锁
}
}
3.5 工作流程
- 检查是否有写锁被其他线程持有
- 如果没有写锁,尝试增加读锁计数
- 如果有写锁被其他线程持有,当前线程进入等待队列
3.2 获取写锁流程
- 检查是否有其他线程持有读锁或写锁
- 如果没有,尝试获取写锁
- 如果有其他线程持有锁,当前线程进入等待队列
4. 高级应用示例
4.1 缓存实现示例
public class CacheWithReadWriteLock<K, V> {
private final Map<K, V> cache = new HashMap<>();
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
// 读取缓存
public V get(K key) {
readLock.lock();
try {
return cache.get(key);
} finally {
readLock.unlock();
}
}
// 写入缓存
public V put(K key, V value) {
writeLock.lock();
try {
return cache.put(key, value);
} finally {
writeLock.unlock();
}
}
// 批量更新缓存
public void putAll(Map<K, V> m) {
writeLock.lock();
try {
cache.putAll(m);
} finally {
writeLock.unlock();
}
}
// 条件更新(读写锁降级示例)
public V putIfAbsent(K key, V value) {
writeLock.lock();
try {
V old = cache.get(key);
if (old != null) {
return old;
}
cache.put(key, value);
return null;
} finally {
writeLock.unlock();
}
}
// 原子更新操作
public V computeIfPresent(K key, BiFunction<K, V, V> remappingFunction) {
writeLock.lock();
try {
V oldValue = cache.get(key);
if (oldValue != null) {
V newValue = remappingFunction.apply(key, oldValue);
if (newValue != null) {
cache.put(key, newValue);
return newValue;
} else {
cache.remove(key);
return null;
}
}
return null;
} finally {
writeLock.unlock();
}
}
}
4.2 数据库连接池示例
public class ConnectionPool {
private final LinkedList<Connection> pool = new LinkedList<>();
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
// 获取连接
public Connection getConnection() {
writeLock.lock(); // 使用写锁确保原子性
try {
while (pool.isEmpty()) {
// 等待可用连接
Thread.sleep(1000);
}
return pool.removeFirst();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
} finally {
writeLock.unlock();
}
}
// 释放连接
public void releaseConnection(Connection conn) {
writeLock.lock();
try {
pool.addLast(conn);
} finally {
writeLock.unlock();
}
}
// 获取当前可用连接数
public int getAvailableCount() {
readLock.lock();
try {
return pool.size();
} finally {
readLock.unlock();
}
}
// 检查特定连接是否在池中
public boolean containsConnection(Connection conn) {
readLock.lock();
try {
return pool.contains(conn);
} finally {
readLock.unlock();
}
}
}
public class ReadWriteLockExample {
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
private int data = 0;
// 读操作
public int readData() {
readLock.lock();
try {
return data;
} finally {
readLock.unlock();
}
}
// 写操作
public void writeData(int value) {
writeLock.lock();
try {
data = value;
} finally {
writeLock.unlock();
}
}
}
5. 性能优化场景
5.1 适用场景
- 读多写少的业务场景
- 读取操作耗时较长的场景
- 需要保证数据一致性的场景
5.2 性能优势
- 提高并发读取效率
- 保证写操作的数据一致性
- 避免写饥饿问题(通过公平锁机制)
6. 注意事项
- 读锁和写锁不能升级或降级
- 避免在持有读锁的情况下申请写锁(可能导致死锁)
- 释放锁的顺序要与获取锁的顺序相反
- 考虑使用 try-finally 确保锁的释放
7. 高级特性
7.1 锁降级
写锁可以降级为读锁,但读锁不能升级为写锁:
rwLock.writeLock().lock();
try {
// 写操作
rwLock.readLock().lock(); // 获取读锁
rwLock.writeLock().unlock(); // 释放写锁,降级为读锁
} finally {
rwLock.readLock().unlock(); // 最后释放读锁
}
7.2 公平性选择
ReentrantReadWriteLock 支持公平和非公平两种模式:
// 公平锁
ReadWriteLock fairLock = new ReentrantReadWriteLock(true);
// 非公平锁(默认)
ReadWriteLock nonFairLock = new ReentrantReadWriteLock(false);
8. 最佳实践
- 合理评估业务场景是否适合使用读写锁
- 正确处理锁的获取和释放
- 避免长时间持有锁
- 考虑使用 StampedLock 作为替代方案(Java 8+)
- 在读多写少的场景下优先考虑读写锁
9. 总结
ReadWriteLock 是一个强大的并发控制工具,它通过分离读写操作的锁定机制,在特定场景下能够显著提升系统性能。但使用时需要注意正确的加锁顺序和锁的释放,以避免死锁等并发问题。在实际应用中,应该根据具体的业务场景和性能需求,合理选择是否使用读写锁。
10. 面试题精选
10.1 基础概念
问题1:什么是ReadWriteLock?它与ReentrantLock相比有什么优势?
答案: ReadWriteLock是Java并发包中提供的一种高级锁机制,它维护了一对锁:读锁和写锁。它允许多个线程同时获取读锁进行共享读取,但只允许一个线程获取写锁进行独占写入。
与ReentrantLock相比的优势:
- ReadWriteLock在读多写少的场景下性能更好,因为它允许多个读线程同时访问共享资源
- ReentrantLock是完全排他的,无论读写操作都会阻塞其他线程
- ReadWriteLock能更好地平衡并发性和线程安全性,提高系统吞吐量
问题2:ReadWriteLock的读锁和写锁分别具有什么特性?
答案: 读锁特性:
- 共享锁:多个线程可以同时持有读锁
- 不排斥其他读锁:一个线程获取读锁不会阻止其他线程获取读锁
- 排斥写锁:持有读锁时,其他线程无法获取写锁
- 可重入:同一线程可以多次获取读锁
写锁特性:
- 独占锁:同一时刻只有一个线程可以持有写锁
- 排斥其他所有锁:持有写锁时,其他线程无法获取任何锁(读锁或写锁)
- 可重入:同一线程可以多次获取写锁
- 可降级:持有写锁的线程可以获取读锁,然后释放写锁,实现锁降级
10.2 实现原理
问题3:ReentrantReadWriteLock是如何使用一个整型变量同时表示读锁和写锁状态的?
答案: ReentrantReadWriteLock巧妙地使用一个32位的int变量来同时表示读锁和写锁的状态:
- 高16位(bit 16-31):表示读锁的持有数量,最多支持65535个读锁
- 低16位(bit 0-15):表示写锁的重入次数,最多支持65535次重入
这种设计通过位运算实现高效的状态管理:
- 获取读锁计数:
c >>> SHARED_SHIFT(右移16位) - 获取写锁计数:
c & EXCLUSIVE_MASK(与掩码进行位与操作) - 增加读锁计数:
c + SHARED_UNIT(加上1«16) - 增加写锁计数:
c + 1
问题4:ReentrantReadWriteLock是如何实现读写互斥的?
答案: ReentrantReadWriteLock通过以下机制实现读写互斥:
-
获取写锁时:
- 检查当前是否有读锁被持有(高16位不为0)
- 检查当前是否有写锁被其他线程持有(低16位不为0且持有线程不是当前线程)
- 如果有任何一种情况存在,则获取写锁失败
-
获取读锁时:
- 检查当前是否有写锁被其他线程持有(低16位不为0且持有线程不是当前线程)
- 如果有,则获取读锁失败
- 特殊情况:如果写锁被当前线程持有,则允许获取读锁(支持锁降级)
10.3 高级特性
问题5:什么是锁降级?为什么ReadWriteLock支持锁降级但不支持锁升级?
答案: 锁降级是指持有写锁的线程获取读锁,然后释放写锁的过程。这样线程就从写锁状态"降级"到了读锁状态。
ReadWriteLock支持锁降级但不支持锁升级的原因:
- 锁降级是安全的:持有写锁意味着线程独占资源,获取读锁后释放写锁不会导致数据不一致
- 锁升级会导致死锁:如果允许读锁升级为写锁,当多个线程同时持有读锁并尝试升级为写锁时,所有线程都无法释放读锁(因为其他线程仍持有读锁),导致死锁
- 锁降级可以保证数据的可见性:在释放写锁之前获取读锁,可以确保线程看到自己的写入结果
问题6:ReentrantReadWriteLock的公平性和非公平性有什么区别?如何选择?
答案: ReentrantReadWriteLock支持公平和非公平两种模式:
公平锁特性:
- 严格按照线程请求的顺序获取锁
- 等待时间最长的线程优先获取锁
- 避免线程饥饿问题
- 通常性能较低,因为需要维护严格的顺序
非公平锁特性(默认):
- 新请求的线程可以"插队"获取锁
- 不保证等待时间与获取锁顺序一致
- 可能导致某些线程长时间等待(饥饿)
- 通常性能较高,因为减少了线程切换
10.4 实际应用
问题7:在什么场景下应该使用ReadWriteLock而不是ReentrantLock?
答案: 适合使用ReadWriteLock的场景:
-
读多写少的业务场景:
- 缓存系统:大量读取操作,少量更新操作
- 配置管理:配置信息频繁读取,偶尔更新
- 数据收集系统:数据频繁读取分析,定期更新
-
读操作耗时较长的场景:
- 复杂数据结构的遍历
- 需要大量计算的数据读取
- 涉及IO操作的读取过程
不适合使用ReadWriteLock的场景:
- 写操作频繁的场景
- 读写操作时间非常短的场景(锁开销可能超过获得的并发收益)
- 读写比例接近1:1的场景
10.5 性能与陷阱
问题8:使用ReadWriteLock时可能遇到的常见问题和陷阱有哪些?
答案: 使用ReadWriteLock时的常见问题和陷阱:
-
锁升级问题:
- 尝试从读锁升级到写锁会导致死锁
- 错误示例:持有读锁时直接申请写锁
-
锁的嵌套顺序问题:
- 在持有写锁的情况下再获取读锁是安全的(锁降级)
- 在持有读锁的情况下再获取写锁会导致死锁
-
锁释放问题:
- 未正确释放锁(忘记在finally块中释放)
- 释放未持有的锁(抛出IllegalMonitorStateException)
-
性能问题:
- 读写锁的开销比普通锁大,在简单场景可能得不偿失
- 长时间持有读锁会阻塞写操作,可能导致写线程饥饿
最佳实践:
- 使用try-finally确保锁的释放
- 避免锁升级操作
- 合理控制锁的持有时间
- 考虑使用Java 8引入的StampedLock作为替代方案
10.6 高级实践
问题9:如何使用ReadWriteLock实现一个线程安全且高性能的缓存预热机制?
public class PrewarmableCache<K, V> {
private final Map<K, V> cache = new HashMap<>();
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
// 数据加载器接口
private final Function<K, V> dataLoader;
// 是否正在预热的标志
private volatile boolean prewarming = false;
public PrewarmableCache(Function<K, V> dataLoader) {
this.dataLoader = dataLoader;
}
// 获取缓存数据
public V get(K key) {
readLock.lock();
try {
V value = cache.get(key);
if (value != null) {
return value;
}
} finally {
readLock.unlock();
}
// 缓存未命中,需要加载数据
writeLock.lock();
try {
// 双重检查
V value = cache.get(key);
if (value == null) {
value = dataLoader.apply(key);
cache.put(key, value);
}
return value;
} finally {
writeLock.unlock();
}
}
// 批量预热缓存
public void prewarm(Collection<K> keys) {
if (prewarming) {
return; // 已经在预热中
}
writeLock.lock();
try {
if (prewarming) {
return; // 双重检查
}
prewarming = true;
// 批量加载数据
Map<K, V> prewarmedData = new HashMap<>();
for (K key : keys) {
if (!cache.containsKey(key)) {
V value = dataLoader.apply(key);
prewarmedData.put(key, value);
}
}
// 批量更新缓存
cache.putAll(prewarmedData);
prewarming = false;
} finally {
writeLock.unlock();
}
}
// 异步预热缓存
public CompletableFuture<Void> prewarmAsync(Collection<K> keys) {
return CompletableFuture.runAsync(() -> prewarm(keys));
}
// 获取缓存大小
public int size() {
readLock.lock();
try {
return cache.size();
} finally {
readLock.unlock();
}
}
// 清除缓存
public void clear() {
writeLock.lock();
try {
cache.clear();
} finally {
writeLock.unlock();
}
}
}
答案: 参考上文中的PrewarmableCache实现示例,关键设计点包括:
- 使用读锁进行常规缓存查询,提高并发读取性能
- 使用写锁进行缓存更新和预热操作,确保数据一致性
- 实现双重检查模式避免重复加载
- 提供同步和异步预热方法,适应不同场景
- 使用volatile标志控制预热状态
- 批量操作时一次获取写锁,减少锁竞争