一句话总结
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
| 特性 | CAS | synchronized |
| 锁类型 | 乐观锁(无锁) | 悲观锁 |
| 线程阻塞 | 不阻塞,自旋重试 | 可能阻塞 |
| 上下文切换 | 无 | 有(重量级锁时) |
| 适用场景 | 竞争不激烈、操作简单 | 竞争激烈、操作复杂 |
一句话总结: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 用途 |
| AtomicInteger | incrementAndGet、compareAndSet |
| AQS | compareAndSetState 修改同步状态 |
| ReentrantLock | CAS 设置 AQS state |
| ConcurrentHashMap | CAS 初始化 table、设置节点 |
| LongAdder | CAS 更新 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。