StampedLock 的乐观读不能直接升级为写锁,因其 stamp 仅为校验标记、不持有锁资源;正确流程是:乐观读失败后降级为悲观读重读,再获取写锁执行修改,并在写锁内二次校验。

StampedLock 的乐观读本身不能直接“转”为写锁,它不支持从 tryOptimisticRead() 获取的 stamp 升级为写锁。这是设计上的明确限制:乐观读只是轻量校验,不持有任何锁资源,因此无法安全升级。要保证一致性,正确做法是「先验证失败 → 降级为悲观读(可选)→ 显式获取写锁」,或更常见的是「绕过乐观读,直接用悲观读 + 写锁协作」。
为什么不能直接乐观读转写锁
乐观读返回的 stamp 是一个特殊标记(非锁持有凭证),仅用于后续 validate() 校验。它不阻塞写操作,也不参与锁状态计数。一旦 validate 失败,说明期间有写入发生,此时原乐观读数据已不可信——强行“升级”会破坏线程安全,JVM 也未提供该 API(tryConvertToWriteLock() 只接受 readLock 或 writeLock 的 stamp,不接受 optimistic stamp)。
保证一致性的标准流程
当业务逻辑需要“读—判断—写”原子性(例如:余额充足才扣款),应按以下步骤组织:
当用户要求为 Java 代码添加注释、修改注释、审查注释、整理注释格式,或要求按照代码注释规范处理 Java 文件时,使用本技能。本技能用于统一生成和维护 Java 类注释模板、按 Git 对比 release 判断版本号、补充每行代码上方中文块注释、调整注释空行规则,并保持 Java 文件 UTF-8 无 BOM 编码。典型触发场景包括:补充类注释、更新 @version、检查 @author 来源、整理行尾注释、把注释放到代码上一行、检查注释是否符合规范、构建前检查 Java 注释风格。
-
先尝试乐观读:快速读取关键字段到局部变量,调用
validate()检查是否被修改 - 验证失败时,获取悲观读锁再重读:确保拿到最新一致快照(避免反复乐观失败)
- 基于最新值做业务判断,再申请写锁执行修改:写锁保证独占,写完立即释放
- 全程使用同一个 stamp 链路:悲观读的 stamp 用于 unlockRead,写锁 stamp 用于 unlockWrite
典型代码结构
以账户扣款为例:
立即学习“Java免费学习笔记(深入)”;
public boolean withdraw(double amount) {
long stamp = sl.tryOptimisticRead();
double currentBalance = balance;
if (!sl.validate(stamp)) {
// 乐观读失效,降级为悲观读
stamp = sl.readLock();
try {
currentBalance = balance;
} finally {
sl.unlockRead(stamp);
}
}
// 业务判断(注意:此时 currentBalance 是可靠快照)
if (currentBalance < amount) {
return false;
}
// 重新获取写锁(悲观读已释放,此处无锁竞争)
long writeStamp = sl.writeLock();
try {
// 再次检查余额(防止其他线程在读写间隙修改)
if (balance >= amount) {
balance -= amount;
return true;
}
return false;
} finally {
sl.unlockWrite(writeStamp);
}
}
关键细节提醒
这段逻辑里有三个容易忽略但至关重要的点:
- 二次校验不可省略:即使刚用悲观读拿到 balance,写锁临界区内仍需再 check 一次。因为悲观读释放后、写锁获取前存在微小窗口
-
悲观读与写锁必须分离:不能在持有悲观读锁时直接调用
writeLock()(会死锁),必须先 unlockRead -
乐观读只适合简单字段读取:不能在乐观读期间调用外部方法或访问对象嵌套字段(如
user.getAddress().getCity()),否则 validate 无法覆盖全部依赖路径

















