Java StampedLock 乐观读校验失败的回退方式
来源:17golang原创
时间:2026-10-10 16:04:31 165浏览 收藏
StampedLock 乐观读校验失败后,不要继续使用第一次读到的局部变量,也不要只循环调用 validate。正确回退方式是获取普通读锁,在读锁保护下把相关共享字段全部重新读取一遍,再在 finally 中用新的读锁 stamp 释放锁。
官方 API 文档:https://docs.oracle.com/en/java/javase/24/docs/api/java.base/java/util/concurrent/locks/StampedLock.html
- 调用
tryOptimisticRead()取得观察 stamp。 - 把需要的共享字段复制到局部变量。
- 调用
validate(stamp)校验观察窗口。 - 失败时获取
readLock(),并在锁内重新读取全部字段。 - 只对真正的读锁 stamp 调用
unlockRead()。
可以直接套用的回退模板
下面的 Counter 同时维护订单数和总金额。读取端要么得到一次有效乐观观察中的两个值,要么回退到读锁后重新取得一致快照。
import java.util.concurrent.locks.StampedLock;
public final class Counter {
private final StampedLock lock = new StampedLock();
private long orderCount;
private long totalAmount;
public Snapshot snapshot() {
// 先取得乐观观察 stamp;写锁正被持有时可能直接返回 0。
long stamp = lock.tryOptimisticRead();
long count = orderCount;
long amount = totalAmount;
if (!lock.validate(stamp)) {
// 校验失败说明观察期间出现过写锁,回退到普通读锁。
stamp = lock.readLock();
try {
// 必须在读锁内重新读取,不能沿用前面可能不一致的局部值。
count = orderCount;
amount = totalAmount;
} finally {
// 此处 stamp 表示真正持有的读锁,因此需要释放。
lock.unlockRead(stamp);
}
}
return new Snapshot(count, amount);
}
public void add(long amount) {
long stamp = lock.writeLock();
try {
// 两个字段作为一个状态整体更新。
orderCount++;
totalAmount += amount;
} finally {
lock.unlockWrite(stamp);
}
}
public static final class Snapshot {
public final long orderCount;
public final long totalAmount;
private Snapshot(long orderCount, long totalAmount) {
// 快照使用不可变字段,返回后不再依赖共享状态。
this.orderCount = orderCount;
this.totalAmount = totalAmount;
}
}
}
当 tryOptimisticRead() 返回 0 时,validate(0) 一定失败,这段代码会自然进入读锁分支,不需要单独再写一套判断。

为什么顺序必须是先读字段、再 validate
validate(stamp) 的含义是:从这个 stamp 产生以后,锁是否被以写模式获取过。校验成功会为此前的乐观读取建立相应的内存可见性保证;它不是一把持续持有的读锁。
如果先 validate,再去读字段,写线程完全可以在校验结束后立刻获取写锁并更新数据。此时后续读取又落在无保护区间,校验结果已经不能覆盖它。
public Snapshot wrongSnapshot() {
long stamp = lock.tryOptimisticRead();
if (lock.validate(stamp)) {
// 错误:校验结束后才读共享字段,写线程可能已在两者之间完成更新。
return new Snapshot(orderCount, totalAmount);
}
// 这里只是示例错误分支,不能作为实际回退方案。
throw new IllegalStateException("optimistic read failed");
}
乐观读不是“先检查现在有没有写线程”,而是对一个已经完成的短观察区间做事后确认。这个区间应尽量短,只做字段读取和轻量局部复制。
为什么获取读锁后还要重新读取
校验失败意味着乐观观察期间有写锁获取过。此前复制到 count 和 amount 的值可能来自不同状态,不能因为现在已经拿到读锁就自动变正确。读锁只能保护拿锁之后的读取,所以要在锁内覆盖全部局部变量。
| 写法 | 问题 | 处理 |
|---|---|---|
| validate 失败后直接返回旧局部变量 | 可能返回混合状态 | 获取读锁并重新读取全部字段 |
| 只重新读取其中一个字段 | 快照仍可能跨越两次状态 | 把同一不变量中的字段作为整体重读 |
| 对乐观 stamp 调用 unlockRead | 乐观 stamp 不代表持有读锁 | 仅在 readLock 成功后解锁 |
| 在失败后反复自旋 validate | 旧 stamp 不会因等待而恢复有效 | 取得新 stamp 重试,或直接回退读锁 |

不能无限等待时,用限时读锁回退
readLock() 可能阻塞。如果读取发生在延迟敏感的接口中,可以使用带超时的 tryReadLock。返回 0 表示在限定时间内没有拿到读锁;线程被中断时要恢复中断标记或继续向上抛出。
import java.util.concurrent.TimeUnit;
public Snapshot snapshotWithin(long timeoutMillis) throws InterruptedException {
long stamp = lock.tryOptimisticRead();
long count = orderCount;
long amount = totalAmount;
if (lock.validate(stamp)) {
// 乐观观察有效,直接返回局部快照。
return new Snapshot(count, amount);
}
// 回退分支最多等待指定毫秒数,避免读线程无限阻塞。
long readStamp = lock.tryReadLock(timeoutMillis, TimeUnit.MILLISECONDS);
if (readStamp == 0L) {
throw new IllegalStateException("无法在限定时间内取得读锁");
}
try {
// 成功持有读锁后重新读取所有相关字段。
return new Snapshot(orderCount, totalAmount);
} finally {
lock.unlockRead(readStamp);
}
}
不要把超时后的异常文案写成“锁已死锁”。StampedLock 不提供公平性保证,短暂写竞争、线程调度和真正的嵌套锁问题都可能导致超时,需要结合调用路径进一步判断。
这些场景不适合直接套乐观读
- 读取会触发副作用:乐观区间可能被放弃,不应在其中修改计数器、发送消息或调用不可重复方法。
- 遍历复杂可变对象图:官方文档提醒乐观读取到的字段可能高度不一致;引用、数组元素和对象方法需要更严格的结构知识。
- 方法可能再次获取同一把锁:
StampedLock不可重入,锁内调用未知方法可能让线程永久等待。 - 写入频率很高:乐观校验经常失败时,反复读取再回退只增加开销,直接读锁可能更简单。
- 需要 Condition:
StampedLock的 Lock 视图不支持newCondition()。
一段可复用的检查清单
- 共享字段是否先复制到局部变量,再调用 validate。
- validate 失败后是否获取读锁并重读全部相关字段。
- 读锁是否始终在 finally 中使用正确 stamp 释放。
- 乐观区间是否只包含短小、只读、可丢弃的操作。
- 是否避免在持锁区调用可能再次获取同一 StampedLock 的方法。
- 高写入场景下是否实际需要乐观读,而不是直接使用读锁。
相关问题
validate 返回 false 之后,能不能继续 validate 同一个 stamp
不能指望旧 stamp 恢复有效。它已经说明观察期间出现过写锁;应重新取得新的乐观 stamp,或按本文模板回退到读锁。
乐观读校验成功就一定看到最新值吗
它保证这次观察窗口没有被写锁获取破坏,并不等价于业务层面的“全局最新”。如果业务要求特定时刻的一致版本,还需要更明确的版本协议。
StampedLock 和 ReentrantReadWriteLock 怎么选
读多写少、读取很短且能安全构造局部快照时,StampedLock 的乐观读才有优势。需要可重入、Condition、清晰所有权或更简单维护时,普通读写锁通常更稳妥。
-
文章 · java教程 | 1个月前 | Java · 异常处理 · 资源管理 · java try-with-resources AutoCloseable close suppressed exception501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
文章 · java教程 | 4小时前 | 异常处理 · 并发编程 · Java教程 · StructuredTaskScope Java Structured Concurrency Joiner allUntil 子任务异常 聚合异常260 收藏
-
121 收藏
-
418 收藏
-
415 收藏
-
文章 · java教程 | 1天前 | spring · Spring Boot · 事务管理 · Java教程 · REQUIRES_NEW Spring事务事件 TransactionalEventListener AFTER_COMMIT Spring事件126 收藏
-
文章 · java教程 | 1天前 | 配置管理 · Java · Spring Boot · 故障排查 · Java教程 · spring boot actuator @ConfigurationProperties 配置属性绑定 PropertySource184 收藏
-
319 收藏
-
418 收藏
-
435 收藏
-
308 收藏
-
265 收藏
-
370 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习