Java进阶篇之StampedLock:先读取,再校验,必要时退回读锁

上一篇用ReadWriteLock把共享读取与独占更新分开了。对于一些字段少、读取短、更新较少的数据,还可以尝试另一种读取路径:先把数据复制到局部变量,检查读取期间有没有写锁介入,再决定这份结果能否使用。

StampedLock提供了这样的乐观读能力。本文基于Java 21,用一对必须保持关系的坐标说明读取、校验与回退;示例关注正确性,不提供未经测量的性能结论。

一、三种模式,先分清谁持有锁

模式 主要方法 使用含义
写锁 writeLock、unlockWrite 独占更新共享数据
读锁 readLock、unlockRead 在保护下读取,阻止写锁进入
乐观读 tryOptimisticRead、validate 取得校验用的stamp,读取后检查有效性

获取读锁或写锁后,返回的stamp需要用于对应的释放操作。乐观读没有取得普通读锁,不能调用unlockRead释放它。

tryOptimisticRead 在当前持有写锁时返回0;validate 检查取得stamp后是否有写锁被获取,stamp为0时校验失败。普通读取路径和写入路径都必须使用同一个StampedLock。契约见Java 21 StampedLock文档。

二、读取结果先放在手里,校验通过再使用

假设两个字段x和y始终满足y等于x的两倍。写线程修改这两个字段时,读线程可能恰好看到一新一旧的组合。因此,乐观读取期间得到的数据暂时只能当作候选值。

图中的凭据对应stamp;"校验"决定候选数据能否交给调用者。发生更新后,旧凭据失效,读取路径转入读锁保护,并重新复制全部需要的字段。

常见流程是:取得stamp→复制字段→validate→成功则使用局部副本,失败则持读锁重新读取。先校验再读取共享字段会留下新的竞争空档,顺序不能颠倒。

三、完整示例:把相关字段一起复制

保存为StampedDemo.java,使用JDK21运行:

java 复制代码
import java.util.concurrent.*;
import java.util.concurrent.locks.StampedLock;

public class StampedDemo {
    record Snapshot(int x, int y) {}

    static class Point {
        private int x = 1;
        private int y = 2;
        private final StampedLock lock = new StampedLock();

        void move(int dx, int dy) {
            long stamp = lock.writeLock();
            try {
                x += dx;
                y += dy;
            } finally {
                lock.unlockWrite(stamp);
            }
        }

        Snapshot snapshot() {
            long stamp = lock.tryOptimisticRead();
            int copyX = x;
            int copyY = y;
            if (!lock.validate(stamp)) {
                stamp = lock.readLock();
                try {
                    copyX = x;
                    copyY = y;
                } finally {
                    lock.unlockRead(stamp);
                }
            }
            return new Snapshot(copyX, copyY);
        }
    }

    public static void main(String[] args) throws Exception {
        Point point = new Point();
        System.out.println("initial: " + point.snapshot());

        // 单独演示:获取凭据后发生写入,旧凭据失效。
        long oldStamp = point.lock.tryOptimisticRead();
        point.move(2, 4);
        System.out.println("old stamp valid: " + point.lock.validate(oldStamp));
        System.out.println("after move: " + point.snapshot());

        try (ExecutorService pool = Executors.newFixedThreadPool(2)) {
            Future<?> writer = pool.submit(() -> {
                for (int i = 0; i < 1000; i++) {
                    point.move(1, 2);
                }
            });
            Future<Integer> reader = pool.submit(() -> {
                for (int i = 0; i < 1000; i++) {
                    Snapshot copy = point.snapshot();
                    if (copy.y() != copy.x() * 2) {
                        throw new AssertionError("inconsistent snapshot: " + copy);
                    }
                }
                return 1000;
            });
            writer.get();
            System.out.println("consistent snapshots: " + reader.get());
        }
    }
}
shell 复制代码
javac StampedDemo.java
java StampedDemo

输出:

text 复制代码
initial: Snapshot[x=1, y=2]
old stamp valid: false
after move: Snapshot[x=3, y=6]
consistent snapshots: 1000

这个示例已在本机JDK21编译运行。前半段固定展示旧stamp失效,后半段检查1000次返回快照的字段关系。线程调度可能不同,输出没有统计乐观读成功率;一次运行也不能代替完整的并发验证或基准测试。

校验成功后使用的是copyX与copyY。这时其他线程仍可能继续写入,但不会改变已经复制出来的int值。Snapshot是包含两个基本类型字段的record,交给调用者后无需继续持锁。

四、为什么HashMap不能照搬这段读取方式

上面的乐观读段只有两个简单字段读取。换成普通HashMap的遍历、查找或复制,需要考虑集合在并发修改过程中的内部状态;调用过程中已经发生的异常或不安全行为,无法靠最后一次validate补救。

乐观读段应保持短小、只读,并能容忍候选字段暂时不一致。读取对象引用后继续调用方法、访问数组元素或做依赖多字段的计算时,应单独分析安全性。难以证明时,直接持读锁读取,或返回经过设计的不可变数据。

尤其不要在校验前基于候选数据发送请求、写文件或更新计数器。即使之后校验失败,这些副作用也已经发生。校验失败时,应丢弃候选值并重新读取。

五、转换、重入与实际取舍

StampedLock不可重入。持有写锁的方法再次调用会获取同一个写锁的方法,可能把自己阻塞住。锁内尽量只做明确的小操作,避免调用未知回调。它也没有直接提供Condition;需要条件等待时应重新选择协调结构。

tryConvertToWriteLock 可以尝试把已有模式转换为写锁,失败返回0。转换失败后,原来持有的读锁仍需正确释放;重新竞争写锁后,再检查业务前提。不要用失败返回值覆盖原stamp后忘记释放原锁。相关使用约定也可对照OpenJDK21的StampedLock源码与注释。

选择时关心的问题 可以考虑的方向
锁内有可重入调用或需要Condition ReentrantLock或ReentrantReadWriteLock
数据简单,读取短,能够安全复制后校验 分析StampedLock的乐观读
数据结构复杂,校验前访问难以保证安全 先用普通读锁或不可变快照
更新频繁,回退较多 测量竞争与回退成本,再决定

"读操作多"只是选型线索。真正影响收益的还有读取长度、写入频率、回退概率和数据表示。把实现复杂度与维护成本一起纳入比较,比只看API名称更有帮助。

六、🧠 思维导图

#mermaid-svg-gFeq3VI6Vsmoppnc{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-gFeq3VI6Vsmoppnc .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gFeq3VI6Vsmoppnc .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gFeq3VI6Vsmoppnc .error-icon{fill:#552222;}#mermaid-svg-gFeq3VI6Vsmoppnc .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gFeq3VI6Vsmoppnc .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gFeq3VI6Vsmoppnc .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gFeq3VI6Vsmoppnc .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gFeq3VI6Vsmoppnc .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gFeq3VI6Vsmoppnc .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gFeq3VI6Vsmoppnc .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gFeq3VI6Vsmoppnc .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gFeq3VI6Vsmoppnc .marker.cross{stroke:#333333;}#mermaid-svg-gFeq3VI6Vsmoppnc svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gFeq3VI6Vsmoppnc p{margin:0;}#mermaid-svg-gFeq3VI6Vsmoppnc .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-gFeq3VI6Vsmoppnc .cluster-label text{fill:#333;}#mermaid-svg-gFeq3VI6Vsmoppnc .cluster-label span{color:#333;}#mermaid-svg-gFeq3VI6Vsmoppnc .cluster-label span p{background-color:transparent;}#mermaid-svg-gFeq3VI6Vsmoppnc .label text,#mermaid-svg-gFeq3VI6Vsmoppnc span{fill:#333;color:#333;}#mermaid-svg-gFeq3VI6Vsmoppnc .node rect,#mermaid-svg-gFeq3VI6Vsmoppnc .node circle,#mermaid-svg-gFeq3VI6Vsmoppnc .node ellipse,#mermaid-svg-gFeq3VI6Vsmoppnc .node polygon,#mermaid-svg-gFeq3VI6Vsmoppnc .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-gFeq3VI6Vsmoppnc .rough-node .label text,#mermaid-svg-gFeq3VI6Vsmoppnc .node .label text,#mermaid-svg-gFeq3VI6Vsmoppnc .image-shape .label,#mermaid-svg-gFeq3VI6Vsmoppnc .icon-shape .label{text-anchor:middle;}#mermaid-svg-gFeq3VI6Vsmoppnc .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-gFeq3VI6Vsmoppnc .rough-node .label,#mermaid-svg-gFeq3VI6Vsmoppnc .node .label,#mermaid-svg-gFeq3VI6Vsmoppnc .image-shape .label,#mermaid-svg-gFeq3VI6Vsmoppnc .icon-shape .label{text-align:center;}#mermaid-svg-gFeq3VI6Vsmoppnc .node.clickable{cursor:pointer;}#mermaid-svg-gFeq3VI6Vsmoppnc .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-gFeq3VI6Vsmoppnc .arrowheadPath{fill:#333333;}#mermaid-svg-gFeq3VI6Vsmoppnc .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-gFeq3VI6Vsmoppnc .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-gFeq3VI6Vsmoppnc .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gFeq3VI6Vsmoppnc .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-gFeq3VI6Vsmoppnc .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gFeq3VI6Vsmoppnc .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-gFeq3VI6Vsmoppnc .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-gFeq3VI6Vsmoppnc .cluster text{fill:#333;}#mermaid-svg-gFeq3VI6Vsmoppnc .cluster span{color:#333;}#mermaid-svg-gFeq3VI6Vsmoppnc div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-gFeq3VI6Vsmoppnc .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-gFeq3VI6Vsmoppnc rect.text{fill:none;stroke-width:0;}#mermaid-svg-gFeq3VI6Vsmoppnc .icon-shape,#mermaid-svg-gFeq3VI6Vsmoppnc .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gFeq3VI6Vsmoppnc .icon-shape p,#mermaid-svg-gFeq3VI6Vsmoppnc .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-gFeq3VI6Vsmoppnc .icon-shape .label rect,#mermaid-svg-gFeq3VI6Vsmoppnc .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gFeq3VI6Vsmoppnc .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-gFeq3VI6Vsmoppnc .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-gFeq3VI6Vsmoppnc :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} StampedLock
三种模式
读锁与写锁
乐观读不持读锁
读取协议
先复制后校验
失败持读锁重读
数据边界
候选值不能产生副作用
谨慎访问可变结构
使用取舍
不可重入
转换失败与性能测量

七、总结

总结要点

乐观读与stamp提供一条先复制、再检查的读取路径。它不阻止写入,候选数据只有通过校验后才能使用。

失败回退与数据封装决定这条路径是否完整。校验失败后持读锁重读全部相关字段,调用者使用局部快照,避免绕开锁协议。

数据表示与使用成本要一起考虑。简单字段适合分析乐观读,复杂可变结构应谨慎;不可重入、转换失败和回退成本都影响实现选择。

下一篇继续讨论AtomicInteger与CAS,看看单个共享值如何完成原子更新,以及它能覆盖哪些业务约束。

👉 如果你觉得这篇文章对你有所帮助,欢迎点赞、收藏、分享!😊

相关推荐
光依旧1 小时前
MCP实战手记(九):生产化MCP Server的6层安全防护
java·人工智能·spring boot·安全·网络安全·ai agent·mcp
阳光九叶草LXGZXJ1 小时前
Linux-学习-12-JDK安装
java·linux·运维·学习
马剑威(威哥爱编程)1 小时前
【AI全栈后端12-04】Spring Boot 用结构化输出自动解析简历:让模型按你的 POJO 输出
java·人工智能·spring boot·后端
阿里嘎多学长2 小时前
2026-09-30 GitHub 热点项目精选
开发语言·程序员·github·代码托管
孙启超3 小时前
【AI开发之Rust】第 22 课:一键多平台与工程收尾 —— CI、发布检查与结课
开发语言·后端·rust
xcl09258 小时前
北京24小时自助健身房系统软件开发实战:从架构到部署全流程指南
java·spring boot·架构
一 乐10 小时前
动漫书销售商城|基于springboot + vue动漫书销售商城(源码+数据库+文档)
java·数据库·vue.js·spring boot·毕业设计
卓怡学长10 小时前
w214基于jsp知道特产网
java·intellij-idea
步行cgn10 小时前
Spring 注解使用详解
java·spring