目录
前言
ReentrantReadWriteLock实现了ReadWriteLock接口。位于java.util.concurrent.locks;
关于更多锁的介绍,可参考:Java常用锁的实践_java常用的锁-CSDN博客
1、普通锁
读写互斥,如 ReentrantLock。
1.1、原理
- 普通锁是排他锁(Exclusive Lock):无论读还是写,同一时刻只能有一个线程持有锁。
- 所有操作互斥:即使多个线程只是读取数据,普通锁也会阻塞其他线程。
代码示例:
ReentrantLock lock = new ReentrantLock();
void read() {
lock.lock();
try {
// 读取数据
} finally {
lock.unlock();
}
}
void write() {
lock.lock();
try {
// 写入数据
} finally {
lock.unlock();
}
}
1.2、特点
- 读线程会阻塞其他读线程:即使没有写操作,读线程之间也不能并发。
- 性能低:在高并发读场景下,资源利用率低。
2、ReadWriteLock
读写分离机制。
- 基于 AQS:通过
state
字段的高位和低位分别管理读锁和写锁。 - 共享锁(Shared):允许多个线程同时读。
- 排他锁(Exclusive):写操作独占锁。
2.1、核心思想
规则:读锁与读锁不互斥。读锁与写锁互斥。写锁与写锁互斥。
-
读锁(共享锁):
- 多个线程可同时持有读锁。
- 获取读锁时,需确保没有写锁存在。
- 读锁可重入(同一线程多次获取读锁时,
state
高位增加)。
-
写锁(排他锁):
- 写锁独占,阻塞所有读和写操作。
- 写锁可重入(同一线程多次获取写锁时,
state
低位增加)。 - 写锁可降级为读锁(但不能升级为写锁)。
-
锁升级/降级规则:
- 不允许升级:读锁不能直接升级为写锁(会破坏公平性,可能导致死锁)。
- 允许降级:写锁可以降级为读锁(需显式释放写锁后获取读锁)。
代码示例:
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
Lock readLock = readWriteLock.readLock();
Lock writeLock = readWriteLock.writeLock();
void read() {
readLock.lock();
try {
// 读取数据(多个线程可同时读)
} finally {
readLock.unlock();
}
}
void write() {
writeLock.lock();
try {
// 写入数据(独占)
} finally {
writeLock.unlock();
}
}
为什么读锁和写锁可以“部分共存”?
- 读锁不阻塞其他读锁:因为读操作不会修改数据,多个线程读取共享数据是安全的。
- 写锁阻塞所有读写:写操作需要独占数据,防止脏读和数据不一致。
2.2、特点
1、高效
适合高并发读的场景。
- 普通锁:多个读线程互相阻塞,吞吐量低。
- 读写锁:多个读线程可并发读取,吞吐量高。
2、缓存读取和更新
class Cache {
private Object data;
private ReadWriteLock lock = new ReentrantReadWriteLock();
void get() {
lock.readLock().lock();
try {
// 多个线程可同时读取
return data;
} finally {
lock.readLock().unlock();
}
}
void put(Object newData) {
lock.writeLock().lock();
try {
// 写入时独占
data = newData;
} finally {
lock.writeLock().unlock();
}
}
}
- 优势:缓存读取频繁,写入较少,使用读写锁可大幅提升并发性能。
2.3、锁共存
写锁不能与读锁或写锁共存。具体是为什么,可参考以下数据一致性和state字段来进行分析。
1. 数据一致性要求
- 写操作必须独占:如果允许写锁与读锁或写锁共存,可能导致:
- 脏读:读线程读到未提交的数据。
- 数据不一致:多个写线程同时修改数据,导致结果不可预测。
2. 内部实现限制
- 读写锁的实现:
- 使用一个
int
类型的state
字段,高16位表示读锁数量,低16位表示写锁重入次数。 - 写锁获取时:必须确保当前没有读锁或写锁。
- 读锁获取时:必须确保当前没有写锁。
- 使用一个
2.4、关键字段
state
:高位(32位)表示读锁数量,低位(32位)表示写锁重入次数。readLock
和writeLock
:分别管理读锁和写锁的获取与释放。
以下是常用的方法:
readLock().lock()
:尝试获取共享锁。writeLock().lock()
:尝试获取排他锁。readLock().unlock()
和writeLock().unlock()
:释放对应锁。
2.5、获取流程
1、写锁
- 检查当前是否有写锁(通过
exclusiveCount
判断)。 - 检查是否有读锁(通过
sharedCount
判断)。 - 如果没有读锁和写锁,则设置写锁状态。
- 否则,将线程加入等待队列。
2、读锁
- 检查当前是否有写锁。
- 如果没有写锁,则尝试增加读锁计数。
- 如果有写锁或读锁溢出,则将线程加入等待队列。
小结
如何选择哪种锁,可根据以下场景进行分析:
-
选择普通锁:
- 数据操作简单(如单次写入后只读)。
- 不需要区分读写操作。
-
选择读写锁:
- 读操作远多于写操作(如缓存、配置中心)。
- 需要提升读并发性能。
对比
普通锁 vs ReadWriteLock:
3、写锁饥饿
3.1、原因
1. 优先级
- ReentrantReadWriteLock 默认是非公平模式(
fair=false
)。 - 读锁的优先级更高:在非公平模式下,读锁可以“插队”获取锁,即使有等待的写线程。
- 写锁需要独占锁:写操作必须阻塞所有读和写,因此写线程会一直等待,直到所有读线程释放读锁。
2. 等待队列机制
- AQS(AbstractQueuedSynchronizer)维护一个 FIFO 队列。
- 非公平模式下:
- 读线程可以“插队”获取锁(无需排队)。
- 写线程只能按顺序等待,直到没有读线程。
示例:
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
// 线程 A: 读线程
lock.readLock().lock();
try {
while (true) {
// 持续读取(不释放读锁)
}
} finally {
lock.readLock().unlock();
}
// 线程 B: 写线程
lock.writeLock().lock(); // 被阻塞,永远无法获取写锁
3.2、实现原理
1. 写锁获取流程
- 检查当前是否有写锁(通过
exclusiveCount
判断)。 - 检查是否有读锁(通过
sharedCount
判断)。 - 非公平模式下:
- 如果没有写锁,且当前线程可以插队(无需等待),则直接获取写锁。
- 如果有读锁或写锁,则将线程加入等待队列。
- 公平模式下:
- 写线程必须按顺序等待,即使没有读锁。
2. 写锁释放流程
- 释放写锁后,唤醒等待队列中的线程。
- 非公平模式下:
- 新来的读线程可能再次插队获取读锁。
- 写线程仍需等待所有读线程释放读锁。
3.3、避免写锁饥饿
1. 使用公平模式(Fair Mode)
- 配置公平锁:
new ReentrantReadWriteLock(true)
。 - 效果:
- 写线程按顺序获取锁,不会被读线程插队。
- 优点:避免写锁饥饿。
- 缺点:性能略低(读线程无法插队)。
代码示例:
ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true); // 公平模式
void read() {
lock.readLock().lock();
try {
// 读取数据
} finally {
lock.readLock().unlock();
}
}
void write() {
lock.writeLock().lock();
try {
// 写入数据
} finally {
lock.writeLock().unlock();
}
}
公平模式下和非公平模式下:
2. 限制读锁的持有时间
- 避免读线程长期占用读锁:
- 在业务逻辑中控制读锁的持有时间。
- 避免在读锁内执行长时间操作。
3. 使用 StampedLock
在Java 8+,StampedLock
提供更灵活的读写锁策略:
- 支持 乐观读锁(不阻塞写锁)。
- 支持 写锁优先级(避免读锁插队)。
代码示例:
StampedLock lock = new StampedLock();
void read() {
long stamp = lock.tryOptimisticRead();
if (lock.validate(stamp)) {
// 乐观读取(不阻塞写锁)
}
}
void write() {
long stamp = lock.writeLock();
try {
// 写入数据
} finally {
lock.unlockWrite(stamp);
}
}
总结:
通过合理选择锁策略,可以在高并发场景下平衡性能与公平性! 😊