Java 基础体系 · 第 65/100 篇。示例统一以 Java 25 LTS 为语言和 JVM 基线;框架示例使用与其兼容的现代稳定版本。
Java Atomic 与 VarHandle:CAS、ABA、内存顺序和无锁边界
java.util.concurrent.atomic 和 VarHandle 都提供了不依赖显式互斥锁的原子访问能力,但“能调用 CAS”并不等于“程序就是无锁的”。要正确使用它们,至少要分清四件事:
- 一次读写是否具有原子性;
- 多线程之间是否能够看见彼此的写入;
- 读写之间具有什么内存顺序;
- 竞争失败后,算法是否仍然能够取得整体进展。
本文以 Java 25 LTS 的 Java 内存模型和标准 API 为基础,逐步说明 Atomic、VarHandle、CAS、ABA 和无锁算法之间的关系。
一、先分开三个容易混淆的概念
1. 原子性:一次操作不能被拆开观察
如果多个线程同时对一个 AtomicInteger 调用 incrementAndGet(),每次递增都是一个不可分割的原子操作:
AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet();
两个线程分别执行一次后,结果一定是 2,不会因为读改写交错而得到 1。
但下面的操作不是原子的:
counter.set(counter.get() + 1);
它包含三个步骤:
读取旧值
计算旧值 + 1
写回新值
两个线程可能产生如下交错:
初始值:0
线程 T1:读取 0
线程 T2:读取 0
线程 T1:计算 1,写回 1
线程 T2:计算 1,写回 1
最终值:1
原子类保证的是单次原子方法的语义,不会自动把多个方法组合成一个事务。
2. 可见性:一个线程的写入能否被另一个线程观察到
class Task {
boolean done;
}
线程 A 执行:
task.done = true;
线程 B 执行:
while (!task.done) {
// 等待
}
如果 done 不是 volatile,也没有锁、线程启动、线程结束或其他同步关系,Java 内存模型不要求线程 B 必须及时观察到 true。
用 volatile 可以建立可见性:
volatile boolean done;
原子类的读取和更新通常也带有相应的 volatile 内存语义,但这不表示所有相关字段都自动变成可见的。可见性必须沿着明确的同步关系传播。
3. 内存顺序:哪些读写必须排在另一些读写之前
即使一个写入最终能被另一个线程看到,仍然需要确定它之前的普通写入是否也能被正确观察。
例如:
int data;
volatile boolean ready;
void writer() {
data = 42;
ready = true;
}
void reader() {
if (ready) {
System.out.println(data);
}
}
根据 Java 内存模型,线程 A 对 ready 的 volatile 写,与线程 B 读取到该写入值的 volatile 读之间建立 happens-before 关系。因此,data = 42 位于 release 一侧,读取 ready 后的代码位于 acquire 一侧,线程 B 能够看到 data 的写入。
这里需要注意两个条件:
- 写入
data必须发生在发布ready之前; - 读取方必须真正读取到相应的发布结果,而不是只读取到旧值。
因此,原子性、可见性和顺序是三个不同维度:
| 问题 | 解决手段示例 |
|---|---|
| 一次加法不能被打断 | AtomicInteger.incrementAndGet() |
| 写入可以被其他线程观察 | volatile、release/acquire、锁 |
| 复杂状态转换不能被并发破坏 | CAS、锁、事务性协议 |
二、Atomic 类解决什么问题
java.util.concurrent.atomic 提供针对常见类型和引用的原子操作:
AtomicIntegerAtomicLongAtomicBooleanAtomicReference<T>AtomicStampedReference<V>AtomicMarkableReference<V>LongAdderLongAccumulator- 以及面向字段更新的
AtomicIntegerFieldUpdater等
AtomicInteger 的核心是“读—计算—条件写回”
下面的 incrementAndGet() 可以抽象为:
for (;;) {
int oldValue = value;
int newValue = oldValue + 1;
if (compareAndSet(oldValue, newValue)) {
return newValue;
}
}
实际 JDK 实现可能使用 VarHandle 或其他内部机制,不能把上面的伪代码当成规定的实现方式,但它准确表达了 CAS 更新的逻辑。
CAS 是 Compare-And-Set 的缩写:
如果当前值 == expected,
就写入 update 并返回成功;
否则,
不写入并返回失败。
形式化地说,设内存位置为 L,期望值为 E,新值为 U:
CAS(L, E, U):
若 L == E:
L := U
返回 true
否则:
L 保持不变
返回 false
这个“比较和写入”必须作为一个不可分割的原子动作执行。
CAS 的线性化点
并发算法经常需要说明某个操作“在哪一刻生效”。这个时刻称为线性化点。
对于成功的 CAS,通常线性化点就是 CAS 成功的瞬间:
T1 读取 old = 10
T2 把值从 10 改为 11
T1 CAS(10, 20) 失败
T1 重新读取 11
T1 CAS(11, 21) 成功
T1 的更新逻辑没有覆盖 T2 的更新,因为它只允许从自己观察到的版本 11 转换到 21。
这也是 CAS 比“先读后写”安全的原因:写入时再次验证了读到的状态仍然有效。
Atomic 方法中的函数可能执行多次
以下代码看似只是传入一个函数:
AtomicInteger value = new AtomicInteger(0);
value.updateAndGet(x -> x + 1);
在竞争下,函数可能被多次调用:
T1 读取 0,计算 1
T2 读取 0,计算 1,CAS 成功
T1 CAS 失败
T1 重新读取 1,再次调用函数,计算 2,CAS 成功
因此,更新函数应当是无副作用的:
value.updateAndGet(x -> {
auditLog.append(x); // 错误:竞争失败时可能重复记录
return x + 1;
});
如果副作用必须只发生一次,就不能把它直接放入可能重试的计算函数中。应当先完成原子状态转换,再根据成功结果处理外部副作用,或者使用其他事务协调机制。
三、VarHandle 是什么
VarHandle 是对变量、数组元素或字段进行多种访问模式操作的统一句柄。它不是某个具体变量的值,而是描述“如何访问某个变量”的对象。
它可以访问:
- 实例字段;
- 静态字段;
- 数组元素;
- 某些字节缓冲区或其他受支持的变量坐标。
VarHandle 将“变量位置”和“内存访问模式”分开,使开发者可以在普通、opaque、acquire/release、volatile 等顺序之间选择。
下面是一个可运行的发布示例:
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
public class VarHandlePublicationDemo {
private static int data;
private static boolean ready;
private static final VarHandle READY;
static {
try {
READY = MethodHandles.lookup()
.findStaticVarHandle(
VarHandlePublicationDemo.class,
"ready",
boolean.class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}
public static void main(String[] args) throws InterruptedException {
Thread writer = new Thread(() -> {
data = 42;
READY.setRelease(true);
});
Thread reader = new Thread(() -> {
while (!(boolean) READY.getAcquire()) {
Thread.onSpinWait();
}
System.out.println(data);
});
reader.start();
writer.start();
writer.join();
reader.join();
}
}
预期输出:
42
关键路径是:
写线程:data = 42
写线程:setRelease(ready, true)
读线程:getAcquire(ready) 读到 true
读线程:读取 data
如果 getAcquire() 读到了 setRelease() 发布的值,那么 release/acquire 关系保证写线程在发布前的普通写入对读线程可见。
代码中的几个失败点也需要明确:
findStaticVarHandle找不到字段或类型不匹配时,会抛出反射相关异常;setRelease和getAcquire的坐标、类型必须匹配,否则可能抛出WrongMethodTypeException;Thread.onSpinWait()只是向运行时提示当前线程处于自旋等待,不会提供同步语义;- 如果没有任何线程最终写入
ready = true,读线程会一直自旋。
四、VarHandle 的内存访问模式
对同一个变量,VarHandle 可以使用不同强度的访问模式。
Plain:普通访问
handle.get(obj);
handle.set(obj, value);
这类似普通字段读写。它不主动建立跨线程同步关系。
如果多个线程并发读写同一个普通变量,且没有其他 happens-before 关系,就可能构成数据竞争。对于复杂对象发布,不能因为“最终大概率读得到”就认为是正确的并发协议。
Opaque:比普通访问更强,但弱于 acquire/release
handle.getOpaque(obj);
handle.setOpaque(obj, value);
Opaque 访问提供比 plain 更强的可观察性和访问一致性约束,但不提供完整的 acquire/release 顺序保证。它适合非常特定的低级协议,例如只需要对某个状态值进行较弱协调,而不要求通过该状态发布一整组之前的普通写入。
不能把 opaque 当作“便宜版 volatile”,也不能把 setOpaque 与 getOpaque 自动当成对象安全发布协议。
Release / Acquire:单向发布和获取
handle.setRelease(obj, value);
handle.getAcquire(obj);
release 约束发布方此前的操作;acquire 约束获取方此后的操作。典型用途是:
生产者先初始化数据,再 release 写入状态
消费者 acquire 读取状态成功后,再读取数据
它们适用于单向发布协议,通常比双方都使用 volatile 更精确。但正确性依赖协议本身,不能只替换某一个访问方法而忽略状态生命周期。
Volatile:最强的常用变量访问语义
handle.getVolatile(obj);
handle.setVolatile(obj, value);
volatile 访问具有更强的跨线程顺序和可见性保证。VarHandle 的普通 CAS 方法也使用相应的 volatile 访问语义;如果需要更弱或更明确的顺序,可以选择带后缀的变体。
常见方法大致可以这样理解:
| 操作 | 语义 |
|---|---|
get / set |
plain |
getOpaque / setOpaque |
opaque |
getAcquire / setRelease |
acquire/release |
getVolatile / setVolatile |
volatile |
compareAndSet |
原子条件更新,volatile 语义 |
compareAndExchangeAcquire |
原子交换,获取语义 |
compareAndExchangeRelease |
原子交换,释放语义 |
weakCompareAndSetPlain 等 |
原子条件更新,但使用指定的较弱内存语义 |
精确选择访问模式前,应先写清楚通信协议:哪个线程发布、哪个线程获取、哪个值表示“发布完成”、失败路径如何处理。只根据性能直觉选择模式,通常会得到难以诊断的数据竞争。
五、CAS 不只是一条指令
在常见硬件上,CAS 可能映射到处理器提供的原子指令;但 Java 规范保证的是 API 的原子语义,不保证每个平台都使用某一条特定机器指令。
一个 CAS 更新通常包含:
1. 读取当前状态;
2. 根据状态计算候选新状态;
3. 原子比较;
4. 比较成功则写入;
5. 失败则重试或返回失败。
因此,CAS 算法的正确性不仅取决于“比较和写入是否原子”,还取决于:
- 比较的状态是否足够完整;
- 状态是否可能被修改后又恢复;
- 重试是否会无限失败;
- 失败时是否会丢失其他线程的更新;
- 更新函数是否可以安全重复执行。
compareAndSet 返回布尔值,适合明确判断成功或失败:
AtomicInteger state = new AtomicInteger(0);
if (state.compareAndSet(0, 1)) {
System.out.println("抢占成功");
} else {
System.out.println("状态已经被其他线程改变");
}
compareAndExchange 则返回观察到的旧值:
int observed = state.compareAndExchange(0, 1);
if (observed == 0) {
System.out.println("更新成功");
} else {
System.out.println("实际观察到:" + observed);
}
这可以减少一次额外读取,尤其适合需要同时处理失败状态的算法。
weakCompareAndSet 允许伪失败,即使当前值等于期望值,也可能返回失败。因此它通常必须放在重试循环中:
for (;;) {
int oldValue = state.get();
int newValue = oldValue + 1;
if (state.weakCompareAndSet(oldValue, newValue)) {
break;
}
Thread.onSpinWait();
}
如果算法不能容忍伪失败,就应使用强 CAS,也就是通常的 compareAndSet。
六、ABA:值相同不代表状态没有变化
ABA 是 CAS 算法中的经典问题。
假设共享引用 top 初始指向节点 A:
top -> A -> B
线程 T1 执行:
oldTop = top; // 读到 A
next = oldTop.next; // 读到 B
T1 暂停。
线程 T2 依次执行:
pop A:top 从 A 改为 B
pop B:top 从 B 改为 null
push A:top 从 null 改回 A
此时结构可能变成:
top -> A
T1 恢复并执行:
CAS(top, A, B)
CAS 只比较当前值是否仍然是 A。它看到的是:
T1 之前读取:A
当前值: A
于是 CAS 成功。但 T1 读取的 B 已经被 T2 移除了,T1 把一个过期的链表后继重新写回了 top:
top -> B
B 可能已经不属于当前栈,甚至在更复杂的手动内存管理环境中已经被回收或复用。
问题不在于 CAS 错误,而在于 CAS 的比较条件太弱:
只验证了“现在还是 A”
没有验证“期间是否发生过 A -> B -> A”
使用 AtomicStampedReference 记录版本
AtomicStampedReference<V> 将引用和整数版本号组合起来比较:
状态 = (引用, stamp)
上面的变化变成:
(A, 10)
(B, 11)
(null, 12)
(A, 13)
T1 最初读到 (A, 10),恢复后执行:
CAS((A, 10), (B, 11))
当前状态是 (A, 13),引用虽然仍是 A,但版本不同,因此 CAS 失败。
下面是一个完整的、可编译的带版本栈示例:
import java.util.concurrent.atomic.AtomicStampedReference;
public class StampedStackDemo<E> {
private static final class Node<E> {
final E value;
final Node<E> next;
Node(E value, Node<E> next) {
this.value = value;
this.next = next;
}
}
private final AtomicStampedReference<Node<E>> top =
new AtomicStampedReference<>(null, 0);
public void push(E value) {
Node<E> newNode;
for (;;) {
int[] stampHolder = new int[1];
Node<E> oldTop = top.get(stampHolder);
int oldStamp = stampHolder[0];
newNode = new Node<>(value, oldTop);
if (top.compareAndSet(
oldTop,
newNode,
oldStamp,
oldStamp + 1)) {
return;
}
Thread.onSpinWait();
}
}
public E pop() {
for (;;) {
int[] stampHolder = new int[1];
Node<E> oldTop = top.get(stampHolder);
if (oldTop == null) {
return null;
}
int oldStamp = stampHolder[0];
Node<E> newTop = oldTop.next;
if (top.compareAndSet(
oldTop,
newTop,
oldStamp,
oldStamp + 1)) {
return oldTop.value;
}
Thread.onSpinWait();
}
}
public static void main(String[] args) {
StampedStackDemo<Integer> stack = new StampedStackDemo<>();
stack.push(1);
stack.push(2);
System.out.println(stack.pop()); // 2
System.out.println(stack.pop()); // 1
System.out.println(stack.pop()); // null
}
}
每次成功修改 top 都递增版本号,因此引用恢复为同一个对象时,版本仍然不同。
但版本号不是无限的。AtomicStampedReference 使用 int 版本,理论上经过足够多次修改后会溢出并再次出现相同版本。如果系统生命周期、修改次数或攻击模型使这种可能性具有现实意义,就需要更宽的版本设计、不可复用的对象身份、锁或其他内存回收方案。
AtomicMarkableReference 解决的是另一类问题
AtomicMarkableReference<V> 将引用和一个布尔标记组合:
(引用, marked)
它适合表示“某引用是否已经被逻辑删除”等一次性状态转换,但布尔标记不能记录任意多次版本变化。因此,它不是 AtomicStampedReference 的等价替代。
七、AtomicReference 不会保护对象内部字段
下面的代码只保证 reference 这个引用的原子更新:
AtomicReference<Account> reference =
new AtomicReference<>(new Account());
它不保证:
reference.get().balance += 100;
是原子的。
如果多个线程取得同一个 Account 对象并修改其普通字段,仍然需要对字段本身建立同步协议。可以选择:
AtomicReference<Account> account;
并使用不可变对象整体替换:
for (;;) {
Account oldAccount = account.get();
Account newAccount = oldAccount.deposit(100);
if (account.compareAndSet(oldAccount, newAccount)) {
break;
}
}
这里的安全性来自:
Account对象创建后不再修改;- 每次更新都基于一个完整快照;
- CAS 确认快照仍然是当前值;
- 失败后重新读取最新快照。
如果 Account 内部仍然可变,引用 CAS 只能保护“换对象”这一步,不能保护对象内部的复合操作。
八、用 VarHandle 自己构造 CAS 状态机
VarHandle 适合需要直接控制字段访问模式的低级组件。例如,一个状态机可以把状态存放在普通字段中,再通过 VarHandle 进行原子转换:
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
public class VarHandleStateDemo {
private static final int NEW = 0;
private static final int RUNNING = 1;
private static final int STOPPED = 2;
private int state = NEW;
private static final VarHandle STATE;
static {
try {
STATE = MethodHandles.lookup()
.findVarHandle(
VarHandleStateDemo.class,
"state",
int.class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}
public boolean start() {
return STATE.compareAndSet(this, NEW, RUNNING);
}
public boolean stop() {
return STATE.compareAndSet(this, RUNNING, STOPPED);
}
public int state() {
return (int) STATE.getVolatile(this);
}
public static void main(String[] args) {
VarHandleStateDemo demo = new VarHandleStateDemo();
System.out.println(demo.start()); // true
System.out.println(demo.start()); // false
System.out.println(demo.stop()); // true
System.out.println(demo.state()); // 2
}
}
start() 的两个线程同时执行时,最多只有一个线程能把 NEW 改成 RUNNING:
线程 T1:CAS(NEW, RUNNING) 成功
线程 T2:CAS(NEW, RUNNING) 失败,因为当前已经是 RUNNING
这里的状态转换具有明确的合法路径:
NEW -> RUNNING -> STOPPED
而不是允许任意写入:
STATE.setVolatile(this, STOPPED);
如果外部代码能够绕过状态机直接写字段,CAS 设计的前置条件就失效了。低级并发组件必须封装字段访问,否则其他代码可能写入算法未预期的状态。
九、内存顺序与 CAS 的组合
原子更新和发布数据经常同时出现。例如生产者要发布一个已经初始化的对象:
class MessageBox {
private Object message;
private boolean published;
private static final VarHandle PUBLISHED;
static {
try {
PUBLISHED = MethodHandles.lookup()
.findVarHandle(MessageBox.class, "published", boolean.class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}
void publish(Object value) {
message = value;
PUBLISHED.setRelease(this, true);
}
Object receive() {
if ((boolean) PUBLISHED.getAcquire(this)) {
return message;
}
return null;
}
}
如果 receive() 返回非空,说明 acquire 读取到了 release 发布的状态,此时 message 的初始化写入也被发布。
但是下面这种写法可能破坏协议:
void publish(Object value) {
PUBLISHED.setRelease(this, true);
message = value; // 太晚,未被 release 发布
}
读线程可能看到 published == true,却不能依赖该 release 操作看到后续的 message 写入。
另一个常见错误是只对数据使用 CAS,却没有定义“数据已经初始化”的状态含义:
reference.compareAndSet(null, object);
如果 object 的构造和内部可变字段的后续初始化没有遵守安全发布规则,那么“引用成功写入”并不等于对象的所有逻辑状态都已经可见。通常应在对象构造完成后再发布,并且对象发布过程使用满足要求的原子或 volatile 语义。
十、无锁、无等待和非阻塞不是同一个词
Lock-free:整体持续取得进展
无锁算法通常指一种进度保证:
无论单个线程是否被挂起,系统中总有某个线程能够在有限步骤内完成操作。
一个典型 CAS 循环是:
for (;;) {
int oldValue = value.get();
int newValue = calculate(oldValue);
if (value.compareAndSet(oldValue, newValue)) {
return newValue;
}
}
当 CAS 失败时,通常意味着其他线程成功更新了共享状态。因此,只要竞争线程持续运行,整体可能持续前进。
但这不是对每个线程公平的保证。某个线程可能持续失败:
T1 成功
T2 失败
T1 成功
T2 失败
……
这称为饥饿或不公平,系统整体可能是 lock-free,但 T2 可能长期得不到进展。
Wait-free:每个线程都有有限上界
Wait-free 更强,要求每个线程都能在有限且通常有上界的步骤内完成操作,不会因为其他线程持续竞争而无限重试。
普通 CAS 自旋循环通常不能直接证明为 wait-free。
Obstruction-free:没有竞争时能完成
Obstruction-free 只要求某个线程独占运行时能够完成。如果其他线程持续竞争,它可能一直失败。
Java API 不自动承诺整个程序是 lock-free
即使底层 AtomicReference 使用了原子硬件操作,整个操作仍可能受到以下因素影响:
- CAS 循环无限重试;
- 线程被操作系统调度出去;
- 线程因安全点、GC 或运行时事件暂停;
- 算法在重试中分配对象;
- 访问内存时发生缓存未命中、缺页或其他系统延迟;
- 失败路径调用了阻塞队列、日志、锁或 I/O。
因此,“算法没有显式 synchronized”只说明源码中没有使用某种锁,不等于整个组件满足严格的无锁进度保证。
某些 Atomic 类提供 isLockFree(),它用于报告当前实现是否具有该类操作的无锁属性,但这仍然不能证明由多个原子操作组成的高层算法是 lock-free,更不能证明业务流程不会阻塞。
十一、自旋 CAS 的边界
自旋适合非常短、竞争有限的更新:
for (;;) {
int oldValue = counter.get();
if (counter.compareAndSet(oldValue, oldValue + 1)) {
return;
}
Thread.onSpinWait();
}
它的失败原因通常是:
读取旧值
其他线程先更新
CAS 发现期望值过期
重新读取
但如果临界逻辑很长,或者竞争激烈,自旋会持续消耗 CPU。此时互斥锁虽然会造成线程阻塞,却可能通过挂起竞争线程减少 CPU 浪费。
CAS 也不能自动解决高竞争计数器的缓存一致性争用。多个线程频繁更新同一个原子变量时,变量所在缓存行会在处理器之间反复转移。统计场景通常可以考虑 LongAdder,但它牺牲了某些“读取瞬间的精确单点值”特性,不适合需要精确 CAS 状态转换的场景。
十二、常见错误及其失败表现
错误一:用 AtomicInteger 替代整个复合条件
if (stock.get() > 0) {
stock.decrementAndGet();
}
两个线程都可能先看到库存为 1,然后都执行递减。修正方式是把“检查和扣减”放进一次 CAS:
for (;;) {
int oldStock = stock.get();
if (oldStock == 0) {
throw new IllegalStateException("库存不足");
}
if (stock.compareAndSet(oldStock, oldStock - 1)) {
break;
}
}
这里 oldStock == 0 是基于本次读取的判断;如果期间库存被其他线程改变,CAS 会失败,循环重新判断。
错误二:把 volatile 当作复合操作原子性
volatile int count;
count = count + 1;
volatile 只强化了读写的可见性和顺序,不会把读、加一、写回合并为原子操作。
错误三:忽略 ABA
AtomicReference<Node> top;
如果节点引用可能被移除后重新放回,单独比较引用可能无法发现中间变化。根据算法需要选择:
- 版本戳;
- 标记位;
- 不可变快照;
- 不复用节点;
- 锁;
- 更完整的生命周期和内存回收协议。
错误四:在 CAS 更新函数中执行副作用
state.updateAndGet(old -> {
metrics.increment();
return old + 1;
});
竞争失败时函数可能重复执行,导致指标多记、消息重复发送或审计记录重复。
错误五:把“CAS 成功”理解成“业务操作成功”
CAS 只表示共享状态从某个期望值转换到了新值。它不代表:
- 数据库事务已经提交;
- 消息已经持久化;
- 网络调用已经成功;
- 外部副作用只执行了一次;
- 其他对象的状态与该变量一致。
如果原子状态和外部系统之间需要一致性,就必须设计补偿、幂等、事务消息或其他跨边界协议。
十三、如何验证并发算法
并发错误往往无法通过一次运行复现。验证时应同时检查状态不变量和进度性质。
例如对无锁栈,可以验证:
1. push 后元素最终可被 pop;
2. 同一个元素不会被两个线程成功 pop;
3. 链表不会出现断链;
4. 空栈 pop 返回 null;
5. 竞争失败不会丢失已提交更新;
6. ABA 防护状态不会被错误接受。
仅依赖:
for (int i = 0; i < 1_000_000; i++) {
// 并发执行
}
不能证明算法正确,因为“没有复现”不是内存模型层面的证据。应结合:
- jcstress 等并发测试工具;
- 随机调度和人为屏障;
- 状态不变量检查;
- 失败重试次数和成功率统计;
- 线程转储与 CPU 采样;
- 高竞争和长时间运行测试。
诊断自旋问题时,重点观察:
CAS 失败率是否很高
单次重试工作是否过长
是否存在固定线程长期饥饿
是否在重试路径分配对象或执行 I/O
是否把阻塞调用隐藏在更新函数中
十四、Atomic、VarHandle 与锁的取舍边界
Atomic 类适合常见、边界清晰的单变量原子状态:
AtomicInteger state;
AtomicReference<Node> head;
VarHandle 适合需要以下能力的组件:
- 直接操作字段或数组元素;
- 对不同路径选择 plain、opaque、acquire/release 或 volatile;
- 实现自定义状态机;
- 减少为一个字段额外包装 Atomic 对象;
- 构造底层并发数据结构。
锁更适合以下情况:
- 一次需要维护多个变量的一致性;
- 操作包含复杂条件和多个步骤;
- 需要阻塞等待、条件队列或公平性;
- 竞争时间较长;
- 算法难以证明不会丢失更新或出现 ABA;
- 外部副作用必须与状态变化放在同一个明确的协调范围内。
“无锁”不是天然更快,也不是天然更可靠。它减少了某类互斥阻塞,却把复杂性转移到了状态表示、内存顺序、重试、饥饿、ABA 和故障恢复上。
结语
Atomic 类提供的是经过封装的原子状态操作;VarHandle 提供的是对变量位置和内存顺序的细粒度控制;CAS 提供的是“只有状态仍然符合预期时才提交更新”的条件转换。
要构造正确的并发算法,需要完整回答以下问题:
共享状态是什么?
一次成功操作的线性化点在哪里?
CAS 比较的状态是否足够完整?
状态是否可能发生 ABA?
发布方和获取方通过什么内存顺序通信?
CAS 失败后如何重试?
重试函数是否允许重复执行?
单个线程是否可能饥饿?
整个业务操作是否包含阻塞或外部副作用?
只有这些问题同时有明确答案时,代码才不仅是“用了 Atomic”或“用了 CAS”,而是真正具有可验证的并发语义。
系列导航与关联阅读
- 系列入口:Java 完整学习路线:从 Java 25 语言与 JVM 到 Spring、微服务和生产交付
- 上一篇:Java Lock、ReadWriteLock、StampedLock 与 Condition:语义和陷阱
- 下一篇:Java CompletableFuture:组合、线程池、超时、取消和异常传播
官方资料
本文依据 Java、Spring 与相关项目官方文档重新梳理;正文、示例与生产清单由 WR BLOG 编写。

评论
0 条讨论