乐观读是带版本校验的无锁读,需用validate()验证stamp有效性,失败后必须降级为readLock()而非重试;stamp严格绑定锁模式,不可混用;适合独立数值字段快照,不适用于复合判断或集合遍历。

乐观读不是“锁”,而是带版本校验的无锁读
StampedLock 的 tryOptimisticRead() 不加锁、不阻塞、不参与锁队列,它只返回一个当前状态戳(stamp),后续靠 validate(stamp) 判断这期间是否发生过写操作。只要没写,读就是零开销;一旦验证失败,说明数据可能已变,必须降级重试。
常见错误是把 tryOptimisticRead() 当成普通读锁用,漏掉 validate() 或在 validate 前就做复杂计算——此时读到的可能是脏值。
- 读取共享变量必须在
validate()成功后,或降级为悲观读之后再执行 - 不能在乐观读期间调用任何可能触发写操作的方法(比如容器的
size()若内部有 lazy-init,就危险) - 局部变量要一次性拷贝完所有依赖字段,避免分步读取导致中间态不一致
validate 失败时必须降级为 readLock(),不能直接重试乐观读
乐观读验证失败(validate() 返回 false)意味着至少一次写操作已发生,但无法确定是哪次、改了什么。此时若继续循环调用 tryOptimisticRead(),可能陷入“验证-失败-再验证”的忙等,浪费 CPU 且不解决一致性问题。
正确做法是立刻调用 readLock() 获取悲观读锁,确保后续读取原子、稳定。
-
readLock()是阻塞式,会排队等待当前写锁释放,但不会饿死写线程(StampedLock 写优先) - 降级后必须用
unlockRead(stamp)配对释放,stamp 来自readLock()而非tryOptimisticRead() - 不要试图在
readLock()保护区内再调用tryOptimisticRead()—— stamp 不可混用,且无意义
写操作必须用 writeLock(),且不能和乐观读 stamp 混用
StampedLock 的 stamp 是单次、单用途、严格绑定锁模式的。写锁返回的 stamp 只能用于 unlockWrite();乐观读返回的 stamp 只能用于 validate();悲观读返回的 stamp 只能用于 unlockRead()。混用会导致 IllegalMonitorStateException 或静默失效。
典型错误是在写方法里误用之前读取的 stamp 调用 unlockRead(),或者把 writeLock() 的 stamp 传给 validate()。
- 每次
writeLock()都生成新 stamp,必须保存并在 finally 中配对unlockWrite() - 写操作中修改的字段,必须是乐观读所依赖的全部字段,否则 validate 无法准确感知变更
- 写操作应尽量轻量:避免在持写锁期间做 I/O、远程调用或长耗时计算
乐观读只适合字段级快照,不适合遍历或条件判断复杂的数据结构
乐观读的本质是“读两遍 + 校验”:第一次快速读字段值,第二次校验是否被改。它假设读取过程本身不改变状态、不依赖中间结果。一旦涉及迭代、求和、查找、条件分支(如 if (x > y) {...}),就极可能因部分字段被写线程更新而产生逻辑错误。
例如对 ConcurrentHashMap 做乐观读 size() + keySet() 遍历,几乎必然失败;又比如读取两个关联字段 a 和 b 后做 a / b 运算,若写线程只改了 b,除零异常就来了。
- 安全场景:读取几个独立、幂等的数值型字段(如坐标
x/y、计数器count、开关标志enabled) - 不安全场景:遍历集合、调用 getter 方法(可能含逻辑)、基于多个字段做复合判断
- 替代方案:这种场景直接用
readLock()更稳妥,性能损失远小于逻辑错误代价
真正难的是判断“哪些读可以乐观”。它不取决于代码多短,而取决于数据依赖是否封闭、是否可重入、是否对部分更新敏感。多数人低估了 validate 失败后的逻辑重构成本——那不只是加一行 readLock(),而是整个读路径要重新考虑线程安全边界。


















