CAS 的原理和 ABA 问题?

2025年 阅读约 10 分钟 面试指南 · Java面试 · Java并发

深入解析CAS(Compare And Swap)原理:乐观锁思想、Unsafe类的CPU原子指令(cmpxchg)、AtomicInteger源码分析、ABA问题的产生和解决方案(AtomicStampedReference/AtomicMarkableReference)、自旋开销与适用场景,附完整源码分析和面试模拟。

一句话总结

CAS(Compare And Swap)是一种无锁的原子操作,包含三个操作数:内存位置 V、预期值 A、新值 B。如果 V 的值等于 A,则将 V 更新为 B,否则不更新。Java 通过 Unsafe 类调用 CPU 的 cmpxchg 指令实现原子性。CAS 是 AtomicInteger、AQS 等 JUC 组件的基石。主要问题:ABA 问题(用版本号解决)、自旋开销(竞争激烈时 CPU 空转)、只能保证单个变量的原子性。

初级理解

CAS 是什么?

CAS 全称 Compare And Swap(比较并交换),是一种乐观锁的实现方式。它假设没有冲突,直接尝试修改,如果发现值被改过就重试。

// CAS 的伪代码 boolean compareAndSwap(V, A, B) { if (V.value == A) { // 比较当前值是否等于预期值 V.value = B; // 是则更新为新值 return true; } return false; // 否则不更新 } // 关键:比较和交换是原子的,由 CPU 指令保证

AtomicInteger 使用示例

AtomicInteger count = new AtomicInteger(0); // incrementAndGet 内部使用 CAS count.incrementAndGet(); // 线程安全的 i++ // compareAndSet 直接使用 CAS boolean success = count.compareAndSet(0, 1); // 如果当前是0,设为1 // getAndUpdate 使用 CAS 循环 count.getAndUpdate(x -> x * 2);

CAS vs synchronized

特性CASsynchronized
锁类型乐观锁(无锁)悲观锁
线程阻塞不阻塞,自旋重试可能阻塞
上下文切换有(重量级锁时)
适用场景竞争不激烈、操作简单竞争激烈、操作复杂
一句话总结:CAS = 比较 + 交换,无锁的原子操作,乐观锁思想的实现。

中级深入

CAS 的底层实现

Java 的 CAS 通过 Unsafe 类的 native 方法实现,最终调用 CPU 的原子指令

// AtomicInteger.incrementAndGet() 源码 public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; } // Unsafe.getAndAddInt() 源码 public final int getAndAddInt(Object o, long offset, int delta) { int v; do { v = getIntVolatile(o, offset); // 获取当前值 } while (!compareAndSwapInt(o, offset, v, v + delta)); // CAS 循环 return v; } // Unsafe.compareAndSwapInt() 是 native 方法 // 最终调用 CPU 的 lock cmpxchg 指令(x86架构) public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);

CPU 如何保证 CAS 原子性?

x86 架构下,CAS 通过 lock cmpxchg 指令实现。lock 前缀会锁定总线或缓存,确保指令执行期间其他 CPU 无法访问该内存地址。多核环境下,通过缓存一致性协议(MESI)保证可见性。

ABA 问题

CAS 只比较值,不关心值的变化过程。如果一个值从 A 变成 B 再变回 A,CAS 检测不到变化:

// ABA 问题示例 // 线程1:取出 A,准备 CAS 设为 C // 线程2:将 A 改为 B // 线程3:将 B 改回 A // 线程1:CAS 成功(值还是 A),但中间已经被改过两次! // 场景:链表头节点 // 初始:head → A → B // 线程1:准备将 head 从 A 改为 C(CAS(head, A, C)) // 线程2:将 head 从 A 改为 B(head → B) // 线程2:将 A 回收,A 的内存被复用 // 线程2:又将 head 从 B 改回 A(head → A) // 线程1:CAS 成功,但 head 指向的 A 已经不是原来的 A 了!

ABA 解决方案 — 加版本号

// AtomicStampedReference:带版本号的 CAS AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(100, 0); // 初始值100,版本0 int stamp = ref.getStamp(); // 获取当前版本号 boolean success = ref.compareAndSet( 100, 200, // 期望值 → 新值 stamp, stamp + 1 // 期望版本 → 新版本 ); // AtomicMarkableReference:简化版,只标记 true/false AtomicMarkableReference<Integer> ref2 = new AtomicMarkableReference<>(100, false);
中级要点:CAS 底层是 CPU 的 lock cmpxchg 指令。ABA 问题通过 AtomicStampedReference(版本号)解决。

高级拓展

CAS 的自旋开销

CAS 失败时会自旋重试(while 循环)。竞争激烈时,大量线程自旋会消耗 CPU。因此 CAS 适合竞争不激烈的场景。JDK 8 的 LongAdder 通过分段累加减少 CAS 竞争。

// LongAdder 的分段思想(简化) // 传统 AtomicLong:所有线程 CAS 同一个变量 → 竞争激烈 // LongAdder:内部有多个 Cell,线程 CAS 不同的 Cell → 减少竞争 // 最终 sum() 时累加所有 Cell 的值 LongAdder adder = new LongAdder(); adder.increment(); // 高并发下性能优于 AtomicLong.incrementAndGet() long sum = adder.sum(); // 获取总和

CAS 只能保证单个变量的原子性

CAS 操作的对象是单个内存地址。如果需要原子地修改多个变量,需要用 AtomicReference 包装一个对象,或者用 synchronized。

CAS 在 JUC 中的应用

CAS 用途
AtomicIntegerincrementAndGet、compareAndSet
AQScompareAndSetState 修改同步状态
ReentrantLockCAS 设置 AQS state
ConcurrentHashMapCAS 初始化 table、设置节点
LongAdderCAS 更新 Cell 值
高级加分项:能说出 CAS 自旋在竞争激烈时的 CPU 开销问题,知道 LongAdder 的分段累加优化,能列举 CAS 在 JUC 中的典型应用。

实战场景

场景一:无锁计数器

public class CasCounter { private AtomicInteger count = new AtomicInteger(0); public void increment() { count.incrementAndGet(); // CAS 保证线程安全 } public int get() { return count.get(); } }

场景二:无锁栈

public class CasStack<T> { private AtomicReference<Node<T>> top = new AtomicReference<>(); public void push(T value) { Node<T> newNode = new Node<>(value); Node<T> oldTop; do { oldTop = top.get(); newNode.next = oldTop; } while (!top.compareAndSet(oldTop, newNode)); // CAS 更新栈顶 } public T pop() { Node<T> oldTop, newTop; do { oldTop = top.get(); if (oldTop == null) return null; newTop = oldTop.next; } while (!top.compareAndSet(oldTop, newTop)); return oldTop.value; } }

常见坑

原因解决方案
ABA 导致数据错乱CAS 只比较值不比较版本AtomicStampedReference
高并发下 CPU 100%大量线程 CAS 自旋改用 LongAdder 或 synchronized
CAS 循环次数过多竞争激烈导致一直失败加退避策略或改用锁

面试模拟

Q:CAS 的原理是什么?

A:CAS 包含三个操作数:内存位置 V、预期值 A、新值 B。如果 V 的值等于 A,则原子地将 V 更新为 B。Java 通过 Unsafe 类的 native 方法实现,底层调用 CPU 的 lock cmpxchg 指令。lock 前缀锁定总线/缓存,保证指令的原子性。

Q:什么是 ABA 问题?如何解决?

A:ABA 问题指一个值从 A 变成 B 再变回 A,CAS 检测不到中间的变化。解决方案是加版本号:AtomicStampedReference 每次更新时版本号 +1,CAS 时同时比较值和版本号。简化版用 AtomicMarkableReference,只标记是否被修改过。

Q:CAS 有什么缺点?

A:三个缺点:1) ABA 问题(用版本号解决);2) 自旋开销:竞争激烈时大量 CPU 空转,LongAdder 通过分段减少竞争;3) 只能保证单个变量原子性:多个变量需要用 AtomicReference 包装或 synchronized。