← 返回题目列表

什么是 CAS?它有什么问题?

高频 中等 第 3 / 31 题 更新于 2026/07/25
CAS原子类ABA并发

简化版

CAS(Compare-And-Swap,比较并交换)是一种无锁的原子操作:它带三个值——内存地址 V、期望的旧值 A、要写的新值 B,只有当 V 处当前值等于 A 时,才把它改成 B,否则不改。它靠 CPU 的原子指令实现,是 AtomicInteger、AQS、乐观锁的底层基石。三个典型问题:ABA 问题、自旋开销、只能保证单个变量原子

详细版

CAS 怎么工作

CAS(V, A, B):  如果 V == A,就把 V 改成 B,返回成功;否则不动,返回失败

它是「乐观」的:不加锁,直接试着改;改之前比一下值有没有被别人动过,没动过(还等于 A)才改。失败了通常自旋重试(再读一次最新值、重新 CAS)。

AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();   // 底层就是 CAS 自旋:读旧值→算新值→CAS,失败就重试

CAS 的三个问题

  1. ABA 问题:值从 A 被改成 B、又改回 A,CAS 检查时以为「没变过」,其实中间经历了变化。
  2. 自旋开销:竞争激烈时大量线程 CAS 失败、不停自旋空转,白耗 CPU。
  3. 只能保证单个变量:CAS 一次只能原子地更新一个变量,多个变量的复合操作它管不了。

完整版教学

一、CAS 为什么能「无锁」还线程安全

传统加锁是「悲观」的:假设一定有人跟我抢,所以先锁住。CAS 是「乐观」的:假设大概率没人抢,直接改,改之前用「比较旧值」来验证期间没被人动过。

关键在于「比较 + 交换」这两步是一条 CPU 原子指令(x86 的 cmpxchg)完成的,中间不可能被别的线程插入。所以它不用加锁就能保证原子性——这就是无锁并发(lock-free)的核心。Java 里通过 Unsafe 类调用这些底层原子指令。

二、ABA 问题:值没变,不代表没被动过

CAS 只看「值等不等于 A」,但值相等不代表没被改过

线程1 读到 V = A,准备 CAS(A → B)……被挂起
线程2 把 V 从 A 改成 B,又改回 A
线程1 恢复,CAS 发现 V 还是 A,成功!但它不知道中间发生过 A→B→A

多数场景 ABA 不影响结果(值一样就行),但在依赖「对象是否被动过」的场景会出错。经典例子是无锁栈:节点被弹出又压回,指针值一样但对象状态已变,可能导致数据错乱。

解决办法:加版本号/时间戳。每次修改版本号 +1,CAS 时同时比较「值 + 版本号」,即使值绕回 A,版本号也变了,就能识破:

AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(100, 0);
// compareAndSet(期望值, 新值, 期望版本, 新版本)——版本对不上就失败
ref.compareAndSet(100, 200, ref.getStamp(), ref.getStamp() + 1);

记忆点:ABA 的解药是版本号,Java 提供了 AtomicStampedReference

三、自旋开销:失败重试的代价

CAS 失败会自旋重试。竞争不激烈时,重试几次就成功,很划算(省了加锁的阻塞唤醒开销)。但竞争激烈时,大量线程同时 CAS,只有一个成功,其余全部失败、不停自旋空转,白白烧 CPU

所以:CAS 适合低到中等竞争;高竞争场景,自旋可能比直接加锁(阻塞让出 CPU)还差。JDK 8 的 LongAdder 就是针对高并发计数对 AtomicLong 的改进——用分段 CAS(多个 Cell 分散热点)减少冲突,是「用空间换低竞争」的思路。

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

CAS 一次只能原子更新一个变量。如果你要「同时原子地更新两个字段」,CAS 做不到。解决办法:

  • 把多个字段包进一个对象,用 AtomicReference 对整个对象引用做 CAS;
  • 或者干脆用锁(synchronized/ReentrantLock)保护这段复合操作。

五、CAS 用在哪

CAS 是 Java 并发的地基,很多东西都建在它上面:

  • 原子类AtomicIntegerAtomicLongAtomicReference 等;
  • AQSReentrantLockCountDownLatchSemaphore 的底层,用 CAS 改 state 和维护队列(详见「AQS 原理」那道题);
  • ConcurrentHashMap:JDK 8 用 CAS 写空桶;
  • 乐观锁:数据库版本号更新、乐观锁本质就是 CAS 思想(详见「乐观锁和悲观锁」那道题)。

六、CAS 的内存语义与失败重试

Java 原子类的 CAS 不只是比较数值,它还带有规定的内存效果。成功的更新既要保证读改写原子性,也要让相关写入按 API 语义对其他线程可见;不同 VarHandle 访问模式还能提供 plain、opaque、acquire/release、volatile 等不同强度。

do {
    old = value;
    next = f(old);
} while (!CAS(value, old, next));

如果 8 个线程同时更新一个热点计数,某次只有 1 个 CAS 成功,其余最多 7 个要重新读取并计算。竞争持续升高时,退避、分段计数或直接使用锁可能比无休止自旋更稳。

七、LongAdder 为什么快,但不能替代所有 AtomicLong

LongAdder 把更新分散到 base 与多个 Cell,线程冲突时尽量落到不同 Cell,求和时再汇总。假设 4 个 Cell 分别为 25、31、22、27,读取 sum() 得到约 105;但读取期间其他线程仍可更新,因此这个结果不是与某一瞬间严格线性化的快照。

需求更合适的选择原因
唯一序号、严格原子读取AtomicLong单点值可线性化更新
高频指标累计LongAdder分散热点,降低 CAS 冲突
多字段约束锁或不可变状态整体 CAS单变量 CAS 无法自动维护不变量

记忆钩子:CAS 解决的是“某个内存位置从期望值原子换成新值”,并不自动解决历史是否变化、多变量一致性和高竞争成本。

八、常见误区与追问

  • 误区:CAS 没有阻塞,所以在任何竞争下都比锁快。 高竞争会让失败线程反复自旋、消耗 CPU 和缓存一致性带宽。
  • 误区:值从 A 回到 A 就代表期间没变化。 这正是 ABA;关心变化历史时要加入版本戳或改用其他同步方案。
  • 误区:LongAdder.sum() 是严格原子的瞬时快照。 它适合统计聚合,读取期间并发更新可能使结果不是线性化快照。
  • 追问:CAS 为什么只能直接保护一个位置? 硬件比较交换针对一个可原子操作的内存单元,多字段约束要封装成一个不可变引用或使用锁。
  • 追问:AtomicStampedReference 怎样解决 ABA? 比较时同时校验引用和版本戳,每次成功修改推进版本。
  • 追问:CAS 失败是否意味着线程会挂起? 原子类常见循环会在用户态重试;是否退避、阻塞取决于上层算法。
  • 追问:CAS 会不会有可见性问题? Java 原子 API 定义了相应内存语义,但具体强度要看调用的方法与 VarHandle 访问模式。

九、加强记忆

CAS 可以记成“比较期望、原子交换、失败重试”:它非常适合短小的单变量状态转换,却要承担自旋和缓存争用成本。ABA 说明“当前值相同”不等于“历史未变化”,需要版本戳等额外状态;多字段不变量也不能靠几个彼此独立的 CAS 自动成立。低到中等竞争下 CAS 往往高效,高竞争统计可用 LongAdder 分散热点,而严格序号或线性化读取仍更适合 AtomicLong。回答时把原子性、内存语义、ABA 和竞争退化四条线连起来,才算真正讲透。