Java StampedLock 乐观读机制如何解决读写锁的写饥饿问题
问题描述:传统读写锁的困境
在并发编程中,当多个线程同时访问共享资源时,我们通常使用锁来保证数据一致性。ReentrantReadWriteLock 是 Java 提供的常见读写锁,它允许多个线程同时持有读锁,但只允许一个线程持有写锁。
这种机制在 读多写少 的场景下效率很高。然而,它存在一个致命缺陷:写饥饿。当持续有读线程获取到读锁时,写线程将永远无法获取到写锁,因为只要有一个读锁被持有,写锁就必须等待。这会导致写操作被无限延迟。
引入 StampedLock 及其乐观读机制,正是为了在保证正确性的前提下,彻底解决写饥饿问题,并大幅提升读多写少场景下的并发性能。
第一步:理解 StampedLock 的核心机制
StampedLock 是 Java 8 引入的一种读写锁,它不同于 ReentrantReadWriteLock 的关键特性是提供了三种锁模式:
- 写锁(Writing):独占锁,和
ReentrantReadWriteLock的写锁类似。 - 悲观读锁(Reading):共享锁,和
ReentrantReadWriteLock的读锁类似。 - 乐观读(Optimistic Reading):这是它的精髓所在,不真正加锁,而是通过一个“版本戳记”来校验数据在读取期间是否被修改。
StampedLock 通过一个 long 类型的状态变量 state 来同时管理这三种模式的锁定状态和版本号。
第二步:掌握乐观读的完整操作流程
乐观读的精髓在于 “先乐观地读,再验证”。整个过程不涉及传统意义上的加锁和解锁,因此不会阻塞任何线程(包括写线程),从而从根本上避免了写饥饿。
以下是执行一次乐观读的标准步骤:
-
调用
tryOptimisticRead()方法。
这个方法不会阻塞。它会立即返回一个long类型的 “戳记”(stamp)。这个戳记本质上是一个版本号,用于记录调用时刻锁的状态。long stamp = stampedLock.tryOptimisticRead(); -
执行实际的读操作,从共享变量中读取所需的数据。
请注意,此时我们没有持有任何锁,写线程仍然可以自由地获取写锁并修改数据。我们读取到的数据可能是一个“中间状态”或“过期状态”。// 假设我们要读取一个复杂数组或对象内部的多个字段 int firstValue = sharedData[0]; int secondValue = sharedData[1]; // ... 其他读取操作 -
校验戳记是否仍然有效。
调用validate(stamp)方法,传入步骤 1 中获取的戳记。该方法会比较当前锁的状态戳记与传入的戳记。- 如果返回
true,说明在步骤 2 读取数据期间,没有任何写锁被获取过,因此我们读取到的数据是一致的、有效的。 - 如果返回
false,说明在步骤 2 执行期间,有写线程成功获取了写锁并修改了数据,我们读取到的数据可能已失效。
if (stampedLock.validate(stamp)) { // 乐观读成功,使用 firstValue, secondValue 等数据 } else { // 乐观读失败,需要采取备用策略(如升级为悲观读锁) } - 如果返回
-
处理校验失败的情况。
当validate返回false时,乐观读失败。此时必须采用一种回退策略来获取有效数据,常见的选择是:- 重试乐观读:如果数据偶尔不一致也可以接受,可以回到步骤 1 重试。
- 升级为悲观读锁:如果必须保证数据一致,应放弃乐观读,转而像使用普通读写锁那样,获取一个真正的悲观读锁。
if (!stampedLock.validate(stamp)) { // 乐观读失败,升级为悲观读锁以确保数据一致性 stamp = stampedLock.readLock(); // 此操作可能阻塞,直到获取到读锁 try { // 在持有悲观读锁的情况下,重新执行安全的数据读取 firstValue = sharedData[0]; secondValue = sharedData[1]; } finally { stampedLock.unlockRead(stamp); // 必须在 finally 块中释放悲观读锁 } }
第三步:对比分析——乐观读如何解决写饥饿
让我们用一张表格来对比传统悲观读和乐观读的核心差异:
| 特性 | 传统悲观读 (ReentrantReadWriteLock) |
乐观读 (StampedLock.tryOptimisticRead) |
|---|---|---|
| 锁获取行为 | 阻塞:必须获取到一个共享的读锁才能开始读操作。 | 非阻塞:立即返回一个版本戳记,不获取任何锁。 |
| 对写线程的影响 | 阻塞写线程:只要至少有一个读锁被持有,写锁请求就会被阻塞,导致写饥饿。 | 零阻塞:乐观读期间,写线程可以自由地获取写锁并修改数据。 |
| 性能开销 | 较高:需要维护锁的获取和释放状态。 | 极低:仅是一次状态戳记的比较(CAS 操作)。 |
| 数据一致性保证 | 强保证:在读锁持有期间,数据不会被修改。 | 弱保证:读到的数据可能已过期,需要通过 validate 校验。 |
| 失败处理 | 无需处理(因为已加锁)。 | 必须处理:校验失败后需决定是重试还是升级为悲观锁。 |
写饥饿的根本原因在于写线程必须等待所有读锁释放。乐观读机制完全移除了“获取读锁”这个步骤。当绝大多数线程都采用乐观读模式时:
- 写线程不再需要等待任何“乐观读线程”,因为它们根本没持锁。
- 写线程只需与可能存在的、正在尝试获取悲观读锁的线程竞争,或者与其他写线程竞争。
- 只要没有线程长期霸占悲观读锁,写线程就能在相对公平的环境下获得写锁,从而彻底避免饥饿。
第四步:实战——实现一个线程安全的并发数据结构
下面是一个使用 StampedLock 保护一个二维点坐标 Point 的完整示例。坐标 x 和 y 必须作为一个整体被读写。
import java.util.concurrent.locks.StampedLock;
public class Point {
private final StampedLock sl = new StampedLock();
private double x;
private double y;
// 写操作:使用排他写锁
public void move(double deltaX, double deltaY) {
long stamp = sl.writeLock(); // 获取写锁
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp); // 释放写锁
}
}
// 读操作(推荐方式):先乐观读,失败再悲观读
public double distanceFromOrigin() {
// 1. 尝试乐观读
long stamp = sl.tryOptimisticRead();
// 2. 读取共享变量到本地变量(快照)
double currentX = x, currentY = y;
// 3. 校验戳记。如果校验失败,说明在读取期间数据被修改过。
if (!sl.validate(stamp)) {
// 4. 校验失败,升级为悲观读锁
stamp = sl.readLock(); // 阻塞直到获取到读锁
try {
// 5. 在锁的保护下,安全地重新读取数据
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp); // 释放悲观读锁
}
}
// 6. 使用有效的快照进行计算
return Math.sqrt(currentX * currentX + currentY * currentY);
}
// 仅使用悲观读锁的旧方法(会导致写饥饿)
public double distanceFromOriginPessimistic() {
long stamp = sl.readLock(); // 获取悲观读锁
try {
return Math.sqrt(x * x + y * y);
} finally {
sl.unlockRead(stamp); // 释放悲观读锁
}
}
}
第五步:识别适用场景与注意事项
适用场景:
- 读多写少:乐观读的优势在写操作很少发生时最为明显。如果写操作频繁,乐观读会频繁失败并回退到悲观读,优势减弱。
- 读操作耗时短:读操作时间越短,在
validate时数据被其他线程修改的概率越低,乐观读成功率越高。 - 对性能有极致要求:需要最小化锁竞争带来的开销。
重要注意事项:
- 不可重入:
StampedLock的锁(包括写锁和悲观读锁)是不可重入的。如果在持有锁的情况下再次获取锁,会导致死锁。 - 不支持条件变量:
StampedLock没有Condition对象,不能调用await()或signal()方法。 - 中断处理:
readLock()和writeLock()方法不能响应中断。如果需要中断支持,请使用readLockInterruptibly()和writeLockInterruptibly()。 - 乐观读不是银弹:它只适用于你能处理“可能读到旧数据”的场景,并且在失败后有明确的回退路径。对于要求强一致性的临界区读操作,仍应使用悲观读锁。
最终建议:优先考虑使用 StampedLock 的乐观读模式来设计你的读多写少并发组件。只有在明确需要可重入性或条件变量时,才退回到 ReentrantReadWriteLock。

暂无评论,快来抢沙发吧!