StampedLock乐观读适用于读多写少、读操作极轻量的场景,要求共享状态不可变或字段用final/volatile修饰,且读操作不依赖多字段强一致性;写操作须严格配对writeLock与unlockWrite。

StampedLock 乐观读的适用场景和前提条件
乐观读不是万能的,它只在读多写少、且读操作本身非常轻量时才真正有效。如果你的读逻辑里包含复杂计算、IO 调用或锁内又嵌套了其他同步块,tryOptimisticRead() 返回的 stamp 几乎必然失效,最终退化为悲观读,反而增加一次 stamp 检查开销。
使用前必须确认:
- 共享状态是不可变对象,或所有字段都是
final或用volatile修饰(否则可能看到部分构造/重排序值) - 读操作不依赖多个字段间的强一致性(比如“余额”和“冻结金额”需同时读准,乐观读无法保证二者快照一致)
- 写操作严格使用
writeLock()+validate(stamp)配对,不能混用lock()等老式锁
怎么正确调用 tryOptimisticRead() 和 validate()
核心是两步:先获取 stamp,再验证是否仍有效。中间不能插入任何可能被写线程干扰的逻辑。
long stamp = lock.tryOptimisticRead();
int current = value; // 仅读字段,不加锁、不调方法、不触发 GC
if (lock.validate(stamp)) {
return current; // 成功,直接返回
}
// 失败,降级为悲观读
stamp = lock.readLock();
try {
return value;
} finally {
lock.unlockRead(stamp);
}
注意点:
-
validate(stamp)必须紧跟在读取操作之后,中间不能有非纯计算语句(如System.out.println()、String.format()) - stamp 为 0 表示当前有写锁持有,此时
validate(0)永远返回 false,但不必特殊处理——后续自然走悲观路径 - 不要缓存 stamp 多次复用,每次读都应重新
tryOptimisticRead()
为什么 writeLock 后必须用 unlockWrite 而不能用 unlock
StampedLock 的 stamp 是带类型信息的长整型,writeLock() 返回的是写锁 stamp,而 unlock() 是 ReentrantLock 的遗留方法,在 StampedLock 中它什么也不做,也不会抛异常,只会悄悄泄漏锁。
常见错误写法:
long stamp = lock.writeLock();
try {
value++;
} finally {
lock.unlock(); // ❌ 无效!value 可能永远被锁住
}
正确写法只有这一种:
long stamp = lock.writeLock();
try {
value++;
} finally {
lock.unlockWrite(stamp); // ✅ 必须匹配
}
漏掉 unlockWrite 的后果比死锁更隐蔽:后续所有 tryOptimisticRead() 都会立即失败,所有读请求被迫走悲观路径,吞吐骤降,但 JVM 不报错、线程不阻塞、监控也难捕获。
乐观读在高竞争下的实际表现和替代思路
当写操作频率超过 ~1%(比如每 100 次读就有 1 次写),乐观读的成功率会快速跌破 50%,此时额外的 stamp 生成、validate 调用、分支预测失败,反而比直接用 readLock() 更耗 CPU。
可以这样判断是否值得用:
- 用 JMH 做对比压测,固定读写比例,看
tryOptimisticRead路径的平均耗时是否显著低于悲观读 - 开启
-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly看热点是否卡在Unsafe.compareAndSwapLong(乐观读底层依赖 CAS)
如果发现 validate 失败率高,又无法降低写频次,不如换用:
-
java.util.concurrent.ConcurrentHashMap(针对 Map 场景) - 不可变容器 +
AtomicReference(如用ImmutableList替代ArrayList并原子更新引用) - 或干脆接受悲观读,把精力放在减少临界区长度上
StampedLock 的乐观读不是性能银弹,它把一致性校验从锁机制转移到了业务代码里,校验逻辑越重,优势越小。










