什么是 CAS?存在什么问题?


一则或许对你有用的小广告

欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书

  • 《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于 Spring AI + Spring Boot 3.x + JDK 21...查看介绍

  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...查看介绍;演示链接:http://116.62.199.48:7070/

  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/

  • 新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍

截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观

面试考察点

  1. 原子操作原理:面试官不仅仅是想知道 CAS 是 "比较并交换",更是想知道你是否理解 CAS 是一条 CPU 原子指令,以及它为什么比加锁更轻量。

  2. 问题识别能力:CAS 并非银弹。ABA 问题、自旋开销、只能操作单个变量——这三大经典问题,至少得说清楚两个,能给出解决方案就是加分项。

  3. 底层实现认知:考察你是否知道 Java 中 CAS 是通过 Unsafe 类调用底层 CPU 指令实现的,比如 x86 架构下的 CMPXCHG 指令。

核心答案

先说结论:CAS(Compare-And-Swap,比较并交换)是一种无锁的原子操作机制。它先比较内存中的值是否等于预期值,如果相等就更新为新值,否则什么都不做。整个操作是原子的,由 CPU 指令级别保证。

CAS 存在三个经典问题:

问题 一句话描述 解决方案
ABA 问题 值从 A 变成 B 又变回 A,CAS 认为没变过 版本号 / AtomicStampedReference
自旋开销 CAS 失败后不断重试,CPU 空转 限制自旋次数 / LongAdder 分散竞争
只能操作单个变量 一次 CAS 只能更新一个值 AtomicReference 包装多个字段

深度解析

一、CAS 的工作原理

CAS 的操作逻辑非常简单,就三步:读取 → 比较 → 交换

上图展示了 CAS 的核心流程。用一句话概括:我觉得值应该是 E,如果是,就把它改成 N;如果不是,说明有人改过了,我不动,重新来

关键在于 "比较" 和 "交换" 这两步被 CPU 封装成了一条 原子指令,中间不可能被打断。x86 架构下用的是 CMPXCHG 指令,ARM 架构下用的是 LDXR/STXR 独占内存指令。

Java 中的 CAS 操作通过 Unsafe 类实现:

// Unsafe.compareAndSwapInt() — JDK 底层 CAS 操作
// 参数:对象、字段偏移量、预期值、新值
public final native boolean compareAndSwapInt(
    Object o, long offset, int expected, int x
);

// AtomicInteger.incrementAndGet() 的实现
public final int incrementAndGet() {
    return U.getAndAddInt(this, VALUE, 1) + 1;
}

// Unsafe.getAndAddInt() — 自旋 CAS
public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);   // 1. 读取当前值
    } while (!compareAndSwapInt(o, offset, v, v + delta));  // 2. CAS 更新,失败则重试
    return v;
}

你看,getAndAddInt() 就是一个典型的自旋 CAS 循环:读值、CAS 更新、失败重试,直到成功为止。没有加锁,没有线程阻塞,所以叫 "无锁编程"。

二、CAS vs 加锁,到底好在哪?

很多人答 CAS 好处只会说 "性能高",但说不清为什么。核心区别在于:

  • 加锁synchronized / ReentrantLock):获取不到锁的线程会被 阻塞挂起,涉及用户态到内核态的切换,开销大
  • CAS:获取不到就 自旋重试,线程始终在用户态运行,不涉及上下文切换

低竞争 场景下,CAS 的性能碾压加锁。但在 高竞争 场景下,大量线程同时自旋,CPU 会飙得很高,这时候反而不如加锁。所以 CAS 并非银弹,它是分场景的。

三、问题一:ABA 问题

这是 CAS 最经典的问题。来看场景:

ABA 问题的本质是:CAS 只认 "值是否相等",不认 "值是否被改过"。

举一个实际的例子:你用 CAS 实现一个无锁栈,线程 1 准备弹出栈顶节点 A,这时被挂起了。线程 2 把 A 弹出,又把 B 压入,又把 A 压回去了。线程 1 醒来执行 CAS,发现栈顶还是 A,成功弹出。但此时 A 的 next 指针已经指向了错误的节点,栈结构被破坏了。

解决方案:加版本号。 每次修改不仅改值,还改版本号。CAS 时同时比较值和版本号:

// JDK 提供的解决 ABA 问题的类
AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(100, 1); // 值=100,版本号=1

// 更新时必须同时匹配值和版本号
int stamp = ref.getStamp(); // 获取当前版本号
ref.compareAndSet(100, 200, stamp, stamp + 1); // 值 100→200,版本号 1→2

// 即使值从 200 改回 100,版本号已经变了,CAS 不会误判

AtomicStampedReference 就是 JDK 给你准备好的 "带版本号的 CAS"。实际开发中,ABA 问题在大多数场景下影响不大(比如纯计数器),但在无锁数据结构(栈、队列、链表)中必须防范。

四、问题二:自旋开销

CAS 失败后要重试,这没问题。但如果竞争非常激烈,大量线程同时自旋,CPU 就在空转:

// 高并发下,大量线程同时执行这个循环
do {
    v = getIntVolatile(o, offset);  // 读
} while (!compareAndSwapInt(o, offset, v, v + delta));  // CAS,失败继续转

想象一下 100 个线程同时自旋,99 个在不断空转消耗 CPU。极端情况下,CPU 占用率能飙到 100%。

解决方案:

  • 自适应自旋(JDK 6+):JVM 根据上次自旋是否成功来动态调整自旋次数,减少无效自旋
  • LongAdder:把竞争分散到多个 Cell,降低单个变量的 CAS 竞争强度
  • 退化为锁ReentrantLock 的底层实现在自旋一定次数后会将线程挂起(park),避免 CPU 空转

五、问题三:只能操作单个变量

一次 CAS 只能更新一个内存位置的值。如果你需要同时原子地更新多个变量,纯 CAS 做不到。

解决方案:把多个变量封装成一个对象,用 AtomicReference 做 CAS。

// 把多个字段封装到一个对象中
class Point {
    final int x, y;
    Point(int x, int y) { this.x = x; this.y = y; }
}

// 用 AtomicReference 做 CAS,一次性更新 x 和 y
AtomicReference<Point> ar = new AtomicReference<>(new Point(1, 2));

Point old = ar.get();
ar.compareAndSet(old, new Point(3, 4)); // x 和 y 原子更新

面试高频追问

  1. CAS 和 synchronized 怎么选?

    低竞争场景用 CAS(原子类),高竞争场景用 synchronizedReentrantLock。JDK 6 之后 synchronized 引入了锁升级(偏向锁 → 轻量级锁 → 重量级锁),轻量级锁阶段本身就用的 CAS,所以两者的边界已经模糊了。实际开发中,能用原子类解决的优先用原子类,逻辑复杂的上锁。

  2. Unsafe 类为什么叫 "Unsafe"?

    因为它可以直接操作内存、绕过 JVM 的安全检查,用错了直接 JVM 崩溃。所以 JDK 9+ 对 Unsafe 做了模块化封装限制,推荐使用 VarHandle 替代。

  3. 乐观锁和悲观锁的区别?

    CAS 是 乐观锁 的实现——认为竞争不激烈,先操作,冲突了再说。synchronized悲观锁 的实现——认为一定有竞争,先加锁再操作。

常见面试变体

  • "CAS 是什么?底层怎么实现的?"
  • "什么是 ABA 问题?怎么解决?"
  • "乐观锁和悲观锁的区别?"
  • "AtomicInteger 是怎么保证线程安全的?"

记忆口诀

CAS 三要素:内存值 V、预期值 E、新值 N(V 等于 E 才更新为 N

三大问题ABA(加版本号)、自旋(限次数/分散竞争)、单变量AtomicReference 包装)

一句话理解:CAS 就是 "我觉得值是 E,是的话改成 N,不是就拉倒重来"

总结

CAS 是 CPU 指令级的原子操作,是 Java 整个并发包的底层基石。它的核心思路是 "无锁编程"——通过自旋重试避免线程阻塞。但它不是银弹,存在 ABA 问题、自旋开销、单变量限制三大经典问题。面试时把原理讲清、把问题说透、把解决方案给出,这道题就稳了。记住口诀:"V 等于 E 才更新为 N,三大问题 ABA、自旋、单变量"