上一篇用AtomicStampedReference记录引用的变化轮次。继续往下看,有些业务只需要表达两个状态:节点还有效,或者节点已被标记删除。把一个布尔值与引用一起观察,就能让提交条件带上这个状态。
今天来看AtomicMarkableReference。我们让一个线程先读取未标记的节点,另一个线程将它标记,再检查旧提交还能不能成功。随后把标记恢复,观察布尔位与版本戳的差别。
本文适用于Java21,示例在JDK21.0.8实际编译运行。这里的节点与删除流程是教学模型,未实现完整并发链表,也没有性能测试数据。
一、一个引用,配一个布尔标记
AtomicMarkableReference 维护引用与boolean标记。应用给标记赋予含义,例如false代表有效、true代表逻辑删除。类本身不会自动解释、翻转或清理这个标记。成对读取与条件更新可参考Java21官方API。
text
初始观察:(A, false)
标记之后:(A, true)
同一个A仍在共享位置里,状态已经变化。持有旧观察的线程如果只比较引用,就会漏掉这个标记;同时比较引用与标记,才能表达"这个节点仍然有效"的提交条件。
二、逻辑删除怎样进入提交条件
调用compareAndSet(expectedReference, newReference, expectedMark, newMark)时,期望引用按引用身份 比较,也就是==;标记也必须匹配,满足后一起更新。对象字段相同或equals()相等,不能代替相同引用。
例如,下面的操作保留A引用,只将false改为true:
java
boolean deleted = state.compareAndSet(a, a, false, true);
对于已经保存(A, false)的读取者,后续提交会因当前标记为true而失败。这表示应用约定的有效性条件改变,并不表示A对象已经被回收。

图中的标牌对应状态位。节点留在原位时也能被标记;将它从数据结构中解除连接,需要另一套完整协议。
三、一次取出同一对状态
需要同时使用引用与标记时,调用get(boolean[] markHolder)。返回值给出引用,数组第一个位置接收对应标记,数组至少有一个元素。
java
boolean[] holder = new boolean[1];
Node old = state.get(holder);
boolean oldMark = holder[0];
boolean changed = state.compareAndSet(old, next, oldMark, false);
分开调用getReference()与isMarked()时,两次读取之间可能发生更新。局部holder与成对读取,让本次提交的观察来源更清楚。CAS失败后应重新检查新状态,再计算下一步。
类还提供attemptMark(expectedReference, newMark),只按期望引用尝试改变标记,没有expectedMark参数 。需要明确检验旧标记时,选用带两个标记参数的CAS更直接。attemptMark文档也提醒可能发生伪失败,不宜把一次false直接解释为引用一定变化。
四、完整示例:标记变化拒绝旧提交
两个CountDownLatch确定先读取、后标记、再提交的顺序。示例后半段主动恢复标记,用来观察状态重复;实际逻辑删除协议可以禁止这个恢复动作。
java
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicMarkableReference;
public class MarkableReferenceDemo {
record Node(String name) {}
public static void main(String[] args) throws Exception {
Node a = new Node("A");
Node b = new Node("B");
AtomicMarkableReference<Node> state =
new AtomicMarkableReference<>(a, false);
CountDownLatch observed = new CountDownLatch(1);
CountDownLatch marked = new CountDownLatch(1);
Thread reader = Thread.ofPlatform().start(() -> {
boolean[] holder = new boolean[1];
Node old = state.get(holder);
boolean oldMark = holder[0];
System.out.println("snapshot=" + old.name() + "/" + oldMark);
observed.countDown();
try {
marked.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
System.out.println("staleCas=" + state.compareAndSet(
old, b, oldMark, false));
});
observed.await();
System.out.println("logicalDelete=" + state.compareAndSet(
a, a, false, true));
marked.countDown();
reader.join();
boolean[] holder = new boolean[1];
Node current = state.get(holder);
System.out.println("afterMark=" + current.name() + "/" + holder[0]);
System.out.println("sameValueOtherObject=" + state.compareAndSet(
new Node("A"), b, true, false));
// Demonstrate a reset; a production deletion protocol may forbid it.
System.out.println("resetMark=" + state.compareAndSet(a, a, true, false));
System.out.println("oldPairMatchesAgain=" + state.compareAndSet(
a, b, false, false));
current = state.get(holder);
System.out.println("final=" + current.name() + "/" + holder[0]);
}
}
保存为MarkableReferenceDemo.java后执行:
bash
javac -encoding UTF-8 MarkableReferenceDemo.java
java MarkableReferenceDemo
本次实际输出:
text
snapshot=A/false
logicalDelete=true
staleCas=false
afterMark=A/true
sameValueOtherObject=false
resetMark=true
oldPairMatchesAgain=true
final=B/false
staleCas=false说明旧观察未通过当前标记的检查。sameValueOtherObject=false说明新建一个字段同为A的Node也无法代替原引用。最后oldPairMatchesAgain=true则揭示布尔位的边界:标记恢复后,原来的那对值再次出现。
五、布尔状态与版本轮次如何选择
| 业务问题 | 更适合表达的状态 |
|---|---|
| 当前节点是否已标记 | 引用与boolean标记 |
| 是否仍是原来那一轮更新 | 引用与应用维护的stamp |
| 多个业务字段必须一起满足规则 | 完整状态、锁或事务等明确协议 |
布尔位记录当前状态。false到true再回false只有两个取值,无法保存每次变化的历史。不能据此宣称AtomicMarkableReference普遍解决所有ABA场景;是否可用,需要分析状态是否允许恢复以及旧观察的生命周期。
逻辑删除需要配套算法。标记后谁负责跳过节点、何时解除连接、并发读写怎样协作,都超出这一对状态的原子更新。本文的Node不包含链表指针,不能直接复制成无锁链表实现。
对象内部仍要有约束。引用与标记一起更新,不会自动保护对象内的可变成员。这里使用只有String字段的record,把示例关注点留在成对状态上。
六、几个上手注意事项
给false与true写清业务含义,并让全部写入遵守同一约定。随意调用set()恢复状态,可能重新放行早先的观察。
CAS只决定这次状态提交是否成功,邮件发送、文件写入等外部操作需要另外安排。将外部动作塞进反复尝试的循环,可能造成多次执行。
观察失败时先检查业务前提。一个已标记节点可能需要停止操作或重新寻找节点,持续盲重试未必能推进业务。
七、🧠 思维导图
#mermaid-svg-8FfI9s0xnFkkip1B{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-8FfI9s0xnFkkip1B .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-8FfI9s0xnFkkip1B .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-8FfI9s0xnFkkip1B .error-icon{fill:#552222;}#mermaid-svg-8FfI9s0xnFkkip1B .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-8FfI9s0xnFkkip1B .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-8FfI9s0xnFkkip1B .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-8FfI9s0xnFkkip1B .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-8FfI9s0xnFkkip1B .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-8FfI9s0xnFkkip1B .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-8FfI9s0xnFkkip1B .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-8FfI9s0xnFkkip1B .marker{fill:#333333;stroke:#333333;}#mermaid-svg-8FfI9s0xnFkkip1B .marker.cross{stroke:#333333;}#mermaid-svg-8FfI9s0xnFkkip1B svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-8FfI9s0xnFkkip1B p{margin:0;}#mermaid-svg-8FfI9s0xnFkkip1B .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-8FfI9s0xnFkkip1B .cluster-label text{fill:#333;}#mermaid-svg-8FfI9s0xnFkkip1B .cluster-label span{color:#333;}#mermaid-svg-8FfI9s0xnFkkip1B .cluster-label span p{background-color:transparent;}#mermaid-svg-8FfI9s0xnFkkip1B .label text,#mermaid-svg-8FfI9s0xnFkkip1B span{fill:#333;color:#333;}#mermaid-svg-8FfI9s0xnFkkip1B .node rect,#mermaid-svg-8FfI9s0xnFkkip1B .node circle,#mermaid-svg-8FfI9s0xnFkkip1B .node ellipse,#mermaid-svg-8FfI9s0xnFkkip1B .node polygon,#mermaid-svg-8FfI9s0xnFkkip1B .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-8FfI9s0xnFkkip1B .rough-node .label text,#mermaid-svg-8FfI9s0xnFkkip1B .node .label text,#mermaid-svg-8FfI9s0xnFkkip1B .image-shape .label,#mermaid-svg-8FfI9s0xnFkkip1B .icon-shape .label{text-anchor:middle;}#mermaid-svg-8FfI9s0xnFkkip1B .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-8FfI9s0xnFkkip1B .rough-node .label,#mermaid-svg-8FfI9s0xnFkkip1B .node .label,#mermaid-svg-8FfI9s0xnFkkip1B .image-shape .label,#mermaid-svg-8FfI9s0xnFkkip1B .icon-shape .label{text-align:center;}#mermaid-svg-8FfI9s0xnFkkip1B .node.clickable{cursor:pointer;}#mermaid-svg-8FfI9s0xnFkkip1B .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-8FfI9s0xnFkkip1B .arrowheadPath{fill:#333333;}#mermaid-svg-8FfI9s0xnFkkip1B .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-8FfI9s0xnFkkip1B .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-8FfI9s0xnFkkip1B .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8FfI9s0xnFkkip1B .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-8FfI9s0xnFkkip1B .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8FfI9s0xnFkkip1B .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-8FfI9s0xnFkkip1B .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-8FfI9s0xnFkkip1B .cluster text{fill:#333;}#mermaid-svg-8FfI9s0xnFkkip1B .cluster span{color:#333;}#mermaid-svg-8FfI9s0xnFkkip1B 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-8FfI9s0xnFkkip1B .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-8FfI9s0xnFkkip1B rect.text{fill:none;stroke-width:0;}#mermaid-svg-8FfI9s0xnFkkip1B .icon-shape,#mermaid-svg-8FfI9s0xnFkkip1B .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8FfI9s0xnFkkip1B .icon-shape p,#mermaid-svg-8FfI9s0xnFkkip1B .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-8FfI9s0xnFkkip1B .icon-shape .label rect,#mermaid-svg-8FfI9s0xnFkkip1B .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8FfI9s0xnFkkip1B .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-8FfI9s0xnFkkip1B .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-8FfI9s0xnFkkip1B :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} AtomicMarkableReference
状态表示
引用与boolean标记
成对读取
get取得同一观察
条件提交
引用身份与旧标记匹配
逻辑删除
标记与解除连接分开设计
适用边界
布尔位不能记录变化轮次
八、总结
总结要点
引用与标记成对使用。先取得同一观察,再将旧引用与旧标记放进CAS条件,标记变化就能拒绝旧提交。
标记承载应用状态。逻辑删除是常见用途,真正的解除连接与并发数据结构仍需要完整算法。
布尔位有表达边界。状态恢复后旧值可以再次匹配;关心变化轮次时,应考虑版本信息与一致的写入协议。
下一篇继续看AtomicIntegerArray,理解数组中单个元素的原子更新,以及它与整组数据一致性的边界。
👉 如果你觉得这篇文章对你有所帮助,欢迎点赞、收藏、分享!😊