Java进阶篇之AtomicStampedReference:引用回到原值,版本戳仍能记住变化

上一篇用AtomicReference发布一份完整状态。接着往下想:线程读到引用A,准备稍后提交;期间另一个线程把A换成B,再换回同一个A。这时,只比较引用还能看见中间那次变化吗?

这个场景常被称为ABA问题。如果业务要求"我读过的状态从未被替换",就需要把变化记录带进提交条件。今天用AtomicStampedReference把引用和版本戳一起观察,再用两个线程把这段时序明确地演示出来。

本文适用于Java21,示例在JDK21.0.8实际编译运行。它是受控的并发教学场景,不代表某个生产系统的压测结果。

一、为什么引用相同,仍可能错过变化

假设一条更新请求建立在旧观察A上。它执行之前,共享位置经历了下面的替换:

text 复制代码
慢线程读取A ── 等待 ── 准备把A换成C
共享位置:A → B → A

最后一个A就是最初那个对象,引用身份完全相同。普通引用CAS此时可以成功,因为它的比较条件仍然满足。CAS按约定完成了比较;业务若还关心"期间没有发生替换",这个条件需要额外表达。

例如,处理一个可循环领取的任务位置时,旧观察可能来自上一次领取周期。眼前对象看起来相同,并不能直接说明这还是原来的那次机会。是否需要防ABA,要看这个中间过程对业务有没有意义。

二、把引用与stamp作为一对状态

AtomicStampedReference 维护一个引用和一个int类型的stamp。调用compareAndSet(expectedReference, newReference, expectedStamp, newStamp)时,当前引用需要与期望引用满足==,当前stamp也需要等于期望stamp;满足后,两者一起更新。它不会调用对象的equals()来比较引用。Java21官方API

本文约定每次有意义的替换都将stamp加一:

text 复制代码
(A, 0) → (B, 1) → (A, 2)
旧观察:(A, 0)
当前值:(A, 2)

慢线程仍认得A,但携带的stamp为0,无法通过当前stamp为2的检查。引用回来以后,旧观察也就无法冒充最新观察。

图中旧票据表示一次已保存的观察;stamp是应用维护的整数,不是时间戳或由类自动生成的流水号。

三、先一次读取,再按观察提交

需要同时使用引用与stamp时,可以调用get(int[] stampHolder)。返回值是这次读取的引用,数组第一个元素装入对应stamp;数组至少有一个位置。读取方法说明

java 复制代码
int[] holder = new int[1];
Item observed = state.get(holder);
int stamp = holder[0];
boolean updated = state.compareAndSet(observed, next, stamp, stamp + 1);

如果先调用getReference(),再调用getStamp(),中间可能发生更新,这两个值就可能来自不同观察时刻。打算一起用于CAS时,应先取得同一对状态。holder建议作为本次操作的局部变量,避免共享数组引出额外竞争。

重新观察也有业务含义。CAS失败后可以重新读取,但应根据新状态重新计算和校验操作,不能只换一个新stamp,再强行提交基于旧状态的决定。

四、完整示例:有序地重现ABA

示例同时设置普通引用与带戳引用。慢线程先读取,主线程再执行往返替换,最后才允许慢线程提交。用两个CountDownLatch安排顺序,不依赖sleep()碰运气。

java 复制代码
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;

public class StampedReferenceDemo {
    record Item(String name) {}

    public static void main(String[] args) throws Exception {
        Item a = new Item("A");
        Item b = new Item("B");
        Item c = new Item("C");
        AtomicReference<Item> plain = new AtomicReference<>(a);
        AtomicStampedReference<Item> stamped =
                new AtomicStampedReference<>(a, 0);
        CountDownLatch snapshotTaken = new CountDownLatch(1);
        CountDownLatch roundTripDone = new CountDownLatch(1);

        Thread slow = Thread.ofPlatform().start(() -> {
            Item oldPlain = plain.get();
            int[] holder = new int[1];
            Item oldStamped = stamped.get(holder);
            int oldStamp = holder[0];
            System.out.println("snapshot=" + oldStamped.name() + "/" + oldStamp);
            snapshotTaken.countDown();
            try {
                roundTripDone.await();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return;
            }
            System.out.println("plainCas=" + plain.compareAndSet(oldPlain, c));
            System.out.println("stampedCas=" + stamped.compareAndSet(
                    oldStamped, c, oldStamp, oldStamp + 1));
        });

        snapshotTaken.await();
        plain.set(b);
        plain.set(a);
        boolean first = stamped.compareAndSet(a, b, 0, 1);
        boolean second = stamped.compareAndSet(b, a, 1, 2);
        System.out.println("roundTrip=" + first + "/" + second);
        roundTripDone.countDown();
        slow.join();

        int[] holder = new int[1];
        Item current = stamped.get(holder);
        System.out.println("plainFinal=" + plain.get().name());
        System.out.println("stampedFinal=" + current.name() + "/" + holder[0]);
    }
}

把代码保存为StampedReferenceDemo.java后运行:

bash 复制代码
javac -encoding UTF-8 StampedReferenceDemo.java
java StampedReferenceDemo

本次实际输出:

text 复制代码
snapshot=A/0
roundTrip=true/true
plainCas=true
stampedCas=false
plainFinal=C
stampedFinal=A/2

roundTrip=true/true说明带戳位置完成了两次更新。普通引用最终变为C;带戳位置保留A/2,慢线程的旧提交被拒绝。示例中的a始终是同一个对象,避免把"值相同的新对象"误当成这里的ABA。

五、几个容易混在一起的边界

关心的问题 需要明确的约定
中间发生过替换吗 所有相关写入都维护stamp
对象内部字段安全吗 使用不可变对象或单独的同步协议
能同时保护多个共享位置吗 一对状态的CAS不等于跨对象事务
stamp能一直增长吗 int有范围,必须考虑回绕与生命周期
失败后可以直接重试吗 重新读取并检查业务前提

写入协议要一致 。调用者传入newStamp,类不会自动递增。若某条路径调用set()恢复旧stamp,或替换对象时保持stamp不变,旧观察可能再次匹配。版本戳的可靠性来自所有相关写入遵守同一个协议。

整数回绕要纳入设计。int的位数有限,长生命周期、高频更新且存在长期挂起观察时,要分析旧戳再次出现的可能性。本文短演示使用0、1、2,不能据此推导长期运行一定安全。

内部可变状态仍需保护。引用与戳一起替换,不会自动冻结对象成员。也不要把这个Java示例理解为通用的原生指针回收方案;内存回收和逻辑状态更新是各自需要分析的问题。

六、什么时候值得使用

当提交必须识别"这还是不是我当时读到的那一轮状态",带戳比较有明确价值。普通计数、整体替换不可变配置等场景,也许现有AtomicInteger或AtomicReference已经足够。

如果规则跨多个字段、多个对象,或重试过程夹着不可撤销的外部操作,锁、事务或业务版本字段往往更容易表达完整约束。选择工具时先写清提交条件,再评估实现复杂度。

七、🧠 思维导图

#mermaid-svg-0149hFl0Pcvtfvd3{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-0149hFl0Pcvtfvd3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0149hFl0Pcvtfvd3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0149hFl0Pcvtfvd3 .error-icon{fill:#552222;}#mermaid-svg-0149hFl0Pcvtfvd3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0149hFl0Pcvtfvd3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0149hFl0Pcvtfvd3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0149hFl0Pcvtfvd3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0149hFl0Pcvtfvd3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0149hFl0Pcvtfvd3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0149hFl0Pcvtfvd3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0149hFl0Pcvtfvd3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0149hFl0Pcvtfvd3 .marker.cross{stroke:#333333;}#mermaid-svg-0149hFl0Pcvtfvd3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0149hFl0Pcvtfvd3 p{margin:0;}#mermaid-svg-0149hFl0Pcvtfvd3 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-0149hFl0Pcvtfvd3 .cluster-label text{fill:#333;}#mermaid-svg-0149hFl0Pcvtfvd3 .cluster-label span{color:#333;}#mermaid-svg-0149hFl0Pcvtfvd3 .cluster-label span p{background-color:transparent;}#mermaid-svg-0149hFl0Pcvtfvd3 .label text,#mermaid-svg-0149hFl0Pcvtfvd3 span{fill:#333;color:#333;}#mermaid-svg-0149hFl0Pcvtfvd3 .node rect,#mermaid-svg-0149hFl0Pcvtfvd3 .node circle,#mermaid-svg-0149hFl0Pcvtfvd3 .node ellipse,#mermaid-svg-0149hFl0Pcvtfvd3 .node polygon,#mermaid-svg-0149hFl0Pcvtfvd3 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0149hFl0Pcvtfvd3 .rough-node .label text,#mermaid-svg-0149hFl0Pcvtfvd3 .node .label text,#mermaid-svg-0149hFl0Pcvtfvd3 .image-shape .label,#mermaid-svg-0149hFl0Pcvtfvd3 .icon-shape .label{text-anchor:middle;}#mermaid-svg-0149hFl0Pcvtfvd3 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-0149hFl0Pcvtfvd3 .rough-node .label,#mermaid-svg-0149hFl0Pcvtfvd3 .node .label,#mermaid-svg-0149hFl0Pcvtfvd3 .image-shape .label,#mermaid-svg-0149hFl0Pcvtfvd3 .icon-shape .label{text-align:center;}#mermaid-svg-0149hFl0Pcvtfvd3 .node.clickable{cursor:pointer;}#mermaid-svg-0149hFl0Pcvtfvd3 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-0149hFl0Pcvtfvd3 .arrowheadPath{fill:#333333;}#mermaid-svg-0149hFl0Pcvtfvd3 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-0149hFl0Pcvtfvd3 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-0149hFl0Pcvtfvd3 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0149hFl0Pcvtfvd3 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-0149hFl0Pcvtfvd3 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0149hFl0Pcvtfvd3 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-0149hFl0Pcvtfvd3 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-0149hFl0Pcvtfvd3 .cluster text{fill:#333;}#mermaid-svg-0149hFl0Pcvtfvd3 .cluster span{color:#333;}#mermaid-svg-0149hFl0Pcvtfvd3 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-0149hFl0Pcvtfvd3 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-0149hFl0Pcvtfvd3 rect.text{fill:none;stroke-width:0;}#mermaid-svg-0149hFl0Pcvtfvd3 .icon-shape,#mermaid-svg-0149hFl0Pcvtfvd3 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0149hFl0Pcvtfvd3 .icon-shape p,#mermaid-svg-0149hFl0Pcvtfvd3 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-0149hFl0Pcvtfvd3 .icon-shape .label rect,#mermaid-svg-0149hFl0Pcvtfvd3 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0149hFl0Pcvtfvd3 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-0149hFl0Pcvtfvd3 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-0149hFl0Pcvtfvd3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} AtomicStampedReference
ABA场景
引用A到B再回A
成对观察
get同时读取引用与戳
条件提交
引用身份与旧戳都匹配
写入协议
应用维护stamp
适用边界
回绕与内部可变状态

八、总结

总结要点

ABA关心变化过程。引用回到同一个对象时,普通引用比较仍会匹配;业务需要识别中间变化,就要将版本信息放进比较条件。

引用和stamp一起使用。先读取同一对状态,再带着旧引用与旧戳提交,失败后重新观察与校验。

协议决定可靠性。stamp由应用维护;整数回绕、对象内部状态和跨对象约束仍要单独设计。

下一篇继续看AtomicMarkableReference,理解一个布尔标记怎样与引用一起表达状态,以及它与版本戳的差异。

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

相关推荐
泡茶喝茶写代码3 小时前
A股量化数据工程:从 REST 接口到策略信号(第 8 篇):成长能力因子:营收与利润增速
java·python·股票数据api·股票数据api接口·股票量化数据api·股票量化数据接口·股票数据api数据
\光辉岁月/3 小时前
7.javase-面向对象
java·开发语言
weixin_531094694 小时前
heap_4内存管理
java·开发语言·算法
程序员南飞4 小时前
win系统安装minio
java·数据结构·算法
Escalating_xu4 小时前
【Python】使用库:标准库、第三方库、pip、项目实战与程序打包
java·python·pip
Wang's Blog5 小时前
Java框架 SpringCloud 快速入门: 过滤器链执行顺序
java·spring cloud
砚底藏山河5 小时前
量化实战:回测数据底座快照与版本管理进阶
java·大数据·python·金融·maven
Cc.Y5 小时前
Java零基础入门:封装与继承 —— 从“裸奔“到“穿衣服“,从“重复造轮子“到“站在巨人肩膀上“
java·开发语言
Escalating_xu5 小时前
【C 语言】深入理解指针(4):回调函数、qsort、void * 与泛型排序的模拟实现
java·c语言·开发语言