登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  java教程

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

最小配方
  1. 调用 tryOptimisticRead() 取得观察 stamp。
  2. 把需要的共享字段复制到局部变量。
  3. 调用 validate(stamp) 校验观察窗口。
  4. 失败时获取 readLock(),并在锁内重新读取全部字段。
  5. 只对真正的读锁 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) 一定失败,这段代码会自然进入读锁分支,不需要单独再写一套判断。

StampedLock 状态、版本、乐观 stamp、局部字段和 validate 结果之间的静态关系
图1:乐观观察依赖锁状态与版本,字段先复制到本地快照,再由 validate 判断这次观察是否可用。

为什么顺序必须是先读字段、再 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 重试,或直接回退读锁
validate 失败、读锁 stamp、共享字段重新读取和最终 Snapshot 的静态分组关系
图2:校验失败后的读锁回退结构;最终 Snapshot 只使用读锁内重新取得的局部值。

不能无限等待时,用限时读锁回退

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、清晰所有权或更简单维护时,普通读写锁通常更稳妥。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>