上一篇用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,理解一个布尔标记怎样与引用一起表达状态,以及它与版本戳的差异。
👉 如果你觉得这篇文章对你有所帮助,欢迎点赞、收藏、分享!😊