CAS通过CPU硬件指令(如x86的CMPXCHG)在一个不可中断周期内原子执行“读值→比较→写值”,依赖volatile保证可见性,无需锁和上下文切换。

CAS 是怎么做到“比较+交换”一步到位的
CAS 不是 Java 语言层面的语法或关键字,而是由 CPU 硬件指令(如 x86 上的 CMPXCHG)直接支持的原子操作。JVM 通过 Unsafe 类暴露了 compareAndSwapInt、compareAndSwapObject 等 native 方法,这些方法最终调用操作系统底层的汇编指令,在一个不可中断的 CPU 周期内完成「读内存值 → 比较 → 写新值」三步——中间不会被线程调度打断。
关键点在于:它依赖 volatile 变量语义保证可见性(每次读都是从主内存取),再靠硬件指令保证原子性。没有锁、没有阻塞、也没有上下文切换开销。
- 不是 Java 字节码实现的,无法在纯 Java 层模拟;必须走
Unsafe+ JNI + 底层汇编链路 - 所有
AtomicInteger、AtomicReference等原子类的incrementAndGet()、compareAndSet()都基于这套机制 - 如果你看到
getAndAddInt方法里那个经典的 do-while 循环,那正是 CAS 失败后自旋重试的体现
为什么 AtomicInteger.addAndGet() 不会丢数据
因为它的底层逻辑本质上是「乐观重试」:先读当前值 oldValue,算出 newValue = oldValue + delta,再用 CAS 尝试把内存位置的值从 oldValue 改成 newValue。只要期间有别的线程抢先改了这个值,CAS 就失败,循环重新读、再算、再试。
public final int addAndGet(int delta) {
int var1;
do {
var1 = this.getIntVolatile(valueOffset);
} while(!this.compareAndSwapInt(valueOffset, var1, var1 + delta));
return var1 + delta;
}
- 如果两个线程同时执行
addAndGet(1),初始值为 0,一个成功把 0→1,另一个读到的是 1 而非 0,CAS 失败后继续重试,最终结果仍是 2 - 这不是“避免竞争”,而是“接受竞争并靠重试解决”——所以高并发下可能自旋很久,吃 CPU
-
volatile保证每次getIntVolatile()都能拿到最新值;否则可能一直读到过期副本,导致无限重试
ABA 问题不是理论陷阱,是真实会崩业务的 Bug
假设一个线程读到值 A,准备 CAS 成 B;但此时另一个线程把 A→B→A 改了两轮。原线程的 CAS 仍会成功,因为它只比数值,不比“有没有被动过”。这在引用类型中尤其危险——比如栈顶节点被弹出又压入同一个对象,表面值没变,实际已不是同一个逻辑状态。
立即学习“Java免费学习笔记(深入)”;
- 典型场景:
AtomicReference管理对象引用时,若对象被复用(如对象池),极易触发 ABA - Java 提供了
AtomicStampedReference和AtomicMarkableReference来带版本号/标记位做双重校验 - 别指望靠加日志或 sleep 规避——ABA 是竞态本质,只能靠带版本的 CAS 或换用锁
别在循环体外手动写 CAS 自旋,除非你真懂代价
很多人看到 CAS 原理后,会手写类似 while (!ref.compareAndSet(old, new)) { old = ref.get(); } 的代码。这看似简洁,但容易忽略几个硬伤:
- 没有退避策略:CPU 空转自旋在高争用下会打满核心,而
AtomicInteger等类内部其实做了轻量级 pause 优化(x86 上是PAUSE指令) - 没考虑线程优先级反转风险:低优先级线程长时间占着 CPU 自旋,高优先级线程反而卡住
- 多数业务场景根本不需要裸 CAS——
ConcurrentHashMap、LongAdder、StampedLock已经封装了更稳的模式
真正该自己上 CAS 的情况极少:比如写高性能 RingBuffer、无锁队列,或定制化状态机。其余时候,优先用标准库提供的原子类或并发容器。










