当前位置:首页 > 文章列表 > 文章 > java教程 > Java StampedLock 乐观读校验失败的回退方式

Java StampedLock 乐观读校验失败的回退方式

来源:17golang原创 2026-10-10 16:04:31 0浏览 收藏

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删除
testing.T.Context 绑定测试清理任务的生命周期testing.T.Context 绑定测试清理任务的生命周期
上一篇
testing.T.Context 绑定测试清理任务的生命周期
模糊测试输入触发 panic 后的复现路径
下一篇
模糊测试输入触发 panic 后的复现路径
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    404次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    481次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    492次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    436次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    262次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码