Java中的读写锁(ReadWriteLock)与StampedLock详解
字数 1748 2025-12-11 11:11:18
Java中的读写锁(ReadWriteLock)与StampedLock详解
一、读写锁(ReadWriteLock)简介
读写锁是一种特殊的锁机制,允许多个线程同时读取共享资源,但只允许一个线程进行写操作。这种设计能显著提升读多写少场景下的并发性能。
核心思想:
- 读锁(共享锁):可被多个线程同时持有
- 写锁(排他锁):同一时刻只能被一个线程持有
- 读写互斥:有写锁时,读锁被阻塞;有读锁时,写锁被阻塞
二、ReadWriteLock接口与ReentrantReadWriteLock实现
1. 接口定义
public interface ReadWriteLock {
Lock readLock(); // 获取读锁
Lock writeLock(); // 获取写锁
}
2. ReentrantReadWriteLock特性
- 支持公平/非公平模式
- 可重入性:线程可重复获取已持有的锁
- 锁降级:写锁可降级为读锁(反之不行)
- 锁升级:不支持读锁升级为写锁(会死锁)
3. 使用示例
class DataCache {
private final Map<String, Object> map = new HashMap<>();
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
public Object get(String key) {
rwLock.readLock().lock(); // 获取读锁
try {
return map.get(key);
} finally {
rwLock.readLock().unlock();
}
}
public void put(String key, Object value) {
rwLock.writeLock().lock(); // 获取写锁
try {
map.put(key, value);
} finally {
rwLock.writeLock().unlock();
}
}
}
三、读写锁的实现原理
1. 状态设计
ReentrantReadWriteLock使用一个32位int同时维护读锁和写锁状态:
- 高16位:读锁计数(共享线程数)
- 低16位:写锁计数(可重入次数)
2. 锁获取规则
- 写锁获取条件:
- 无任何锁(state == 0)
- 当前线程已持有写锁(可重入)
- 读锁获取条件:
- 无写锁(state低16位为0)
- 公平模式下检查等待队列
3. 关键方法源码解析
// 写锁尝试获取
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");
}
if (writerShouldBlock() || // 公平性检查
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
四、读写锁的局限性
- 写锁饥饿:大量读线程可能导致写线程长时间等待
- 性能下降:高竞争时CAS操作频繁
- 不支持乐观读:读操作也需要获取锁
五、StampedLock的引入
Java 8引入的增强型读写锁,解决了传统读写锁的部分问题。
1. 三种访问模式
- 写锁(Writing):独占锁,类似ReentrantReadWriteLock.WriteLock
- 悲观读锁(Reading):共享锁,类似ReentrantReadWriteLock.ReadLock
- 乐观读(Optimistic Reading):无锁操作,通过验证确保数据一致性
2. 核心优势
- 乐观读避免锁开销
- 支持锁升级/降级
- 更好的吞吐量
六、StampedLock使用详解
1. 基本用法
class Point {
private double x, y;
private final StampedLock sl = new StampedLock();
// 写操作
void move(double deltaX, double deltaY) {
long stamp = sl.writeLock(); // 获取写锁
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp); // 释放写锁
}
}
// 悲观读
double distanceFromOrigin() {
long stamp = sl.readLock(); // 获取读锁
try {
return Math.sqrt(x * x + y * y);
} finally {
sl.unlockRead(stamp);
}
}
// 乐观读
double distanceFromOriginOptimistic() {
long stamp = sl.tryOptimisticRead(); // 尝试乐观读
double currentX = x, currentY = y;
if (!sl.validate(stamp)) { // 检查期间是否有写操作
stamp = sl.readLock(); // 升级为悲观读
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
}
2. 锁转换示例
// 读锁升级为写锁
long stamp = sl.readLock();
try {
while (condition) {
long ws = sl.tryConvertToWriteLock(stamp);
if (ws != 0L) {
stamp = ws;
// 执行写操作
break;
} else {
sl.unlockRead(stamp);
stamp = sl.writeLock();
}
}
} finally {
sl.unlock(stamp);
}
七、StampedLock实现原理
1. 状态设计
使用long类型state维护状态:
- 低7位:写锁状态和读锁计数
- 第8位:写锁占用标志
- 高56位:读锁计数和版本号
2. 乐观读实现机制
public long tryOptimisticRead() {
long s = state;
// 返回版本号(高56位),如果低8位有写锁则返回0
return (s & WBIT) == 0L ? (s & RBITS) : 0L;
}
public boolean validate(long stamp) {
// 比较当前状态与乐观读时获取的版本号
// 通过内存屏障确保读取顺序
U.loadFence();
return (stamp & SBITS) == (state & SBITS);
}
3. 内存屏障使用
- loadFence():确保validate前的加载操作不会重排序
- storeFence():确保写锁释放前的存储操作可见
八、对比总结
| 特性 | ReentrantReadWriteLock | StampedLock |
|---|---|---|
| 锁类型 | 读锁、写锁 | 读锁、写锁、乐观读 |
| 可重入 | 支持 | 不支持 |
| 锁升级 | 不支持 | 支持 |
| 公平模式 | 支持 | 不支持 |
| 条件变量 | 支持 | 不支持 |
| 性能 | 中等 | 高并发下更好 |
| 死锁风险 | 较低 | 使用不当易死锁 |
九、使用建议与注意事项
1. 选择依据
- 选ReentrantReadWriteLock:
- 需要可重入特性
- 需要条件变量支持
- 代码复杂度要求低
- 选StampedLock:
- 读多写少且读操作耗时短
- 对性能要求极高
- 能妥善处理锁转换
2. 注意事项
- StampedLock不可重入:同一线程重复获取会死锁
- 必须释放锁:finally块中确保unlock
- 避免长时间持有锁:特别是写锁
- 验证乐观读:必须调用validate检查
- 小心锁转换:转换失败需正确处理
3. 最佳实践示例
class CachedData {
private Object data;
private final StampedLock lock = new StampedLock();
public Object read() {
// 1. 尝试乐观读
long stamp = lock.tryOptimisticRead();
Object currentData = data;
// 2. 验证数据一致性
if (!lock.validate(stamp)) {
// 3. 验证失败,升级为悲观读
stamp = lock.readLock();
try {
currentData = data;
} finally {
lock.unlockRead(stamp);
}
}
return currentData;
}
}
十、总结
读写锁通过分离读/写操作提升了并发性能,而StampedLock通过乐观读机制进一步优化。在实际开发中,应根据具体场景选择:
- 简单场景用ReentrantReadWriteLock
- 高性能场景用StampedLock
- 始终注意锁的获取顺序和释放,避免死锁
这两种锁都是Java并发工具包中的重要组件,理解其原理和适用场景,能帮助我们在实际开发中做出更合适的技术选型。