
🍃 予枫 :个人主页
📚 个人专栏 : 《Java 从入门到起飞》《读研码农的干货日常》《Java 面试刷题指南》
💻 Debug 这个世界,Return 更好的自己!
引言
家人们谁懂啊!Java并发面试里,CAS绝对是"常驻嘉宾",不管是初级还是中级面试官,必问一句"你说说CAS的核心原理"。很多人只记个"比较并交换"的表面,被追问ABA问题、优缺点时直接卡壳。今天就从底层原理、代码实战、面试追问三个维度,把CAS讲透,让你下次面试遇到它,能侃侃而谈!
文章目录
- 引言
- 一、CAS是什么?一句话讲懂核心
- 二、CAS核心原理拆解(结合源码更易懂)
-
- [2.1 Java中CAS的实战示例(AtomicInteger)](#2.1 Java中CAS的实战示例(AtomicInteger))
- [2.2 CAS执行流程(mermaid图直观展示)](#2.2 CAS执行流程(mermaid图直观展示))
- [2.3 底层源码窥探(AtomicInteger的getAndIncrement)](#2.3 底层源码窥探(AtomicInteger的getAndIncrement))
- 三、CAS的优缺点(面试必问,别只说一半)
-
- [3.1 优点](#3.1 优点)
- [3.2 缺点](#3.2 缺点)
- 四、ABA问题(核心重点,面试高频)
-
- [4.1 什么是ABA问题?](#4.1 什么是ABA问题?)
- [4.2 ABA问题的危害](#4.2 ABA问题的危害)
- [4.3 ABA问题的3种解决方案](#4.3 ABA问题的3种解决方案)
-
- 方案1:使用版本号(最常用)
- [方案2:使用 AtomicStampedReference(Java 提供的现成工具类)](#方案2:使用 AtomicStampedReference(Java 提供的现成工具类))
- 方案3:禁止值回滚(场景受限)
- 五、面试官追问环节
-
- [追问1:CAS 和 synchronized 的区别?](#追问1:CAS 和 synchronized 的区别?)
- [追问2:AtomicInteger 为什么不用 synchronized 而用 CAS?](#追问2:AtomicInteger 为什么不用 synchronized 而用 CAS?)
- 追问3:ABA问题的本质是什么?怎么避免?
- 六、总结
一、CAS是什么?一句话讲懂核心
CAS 全称 Compare And Swap(比较并交换),是一种无锁并发编程机制,核心思想是"先比较,再交换",无需加锁就能保证并发安全,也是 Atomic 系列工具类(如 AtomicInteger)的底层实现核心。
简单来说,CAS 操作包含三个关键参数:
- 内存地址 V(要操作的变量在内存中的地址)
- 预期值 A(线程之前读取到的变量值)
- 新值 B(线程要修改成的目标值)
执行逻辑特别好记:线程修改变量时,先判断内存中 V 地址的值是否等于预期值 A。如果相等,就把 V 的值改成 B;如果不相等,说明变量被其他线程修改过,当前线程不做任何操作,直接重试(也可以放弃)。
举个生活化的例子:你去超市买水,货架上只剩1瓶(内存值V=1),你心里预期它还在(A=1),准备拿它结账(改B=0);但如果有人比你快一步拿走了(V变成0),你发现预期A≠V,就只能重新去货架确认(重试)。

二、CAS核心原理拆解(结合源码更易懂)
CAS 的底层实现依赖于 CPU 提供的原子指令(如 x86 架构的 cmpxchg 指令),因为 CPU 级别的指令是原子性的,能避免多线程并发修改时的竞态问题,这也是 CAS 无锁安全的核心原因。
2.1 Java中CAS的实战示例(AtomicInteger)
我们平时用的 AtomicInteger,就是基于 CAS 实现的,来看一段简单代码,感受下 CAS 的实际应用:
java
public class CASDemo {
public static void main(String[] args) {
// 初始值为10
AtomicInteger atomicInteger = new AtomicInteger(10);
// 比较并交换:预期值10,新值20,返回是否成功
boolean result1 = atomicInteger.compareAndSet(10, 20);
System.out.println("第一次CAS:" + result1 + ",当前值:" + atomicInteger.get()); // true,20
// 再次CAS:预期值10(已被修改),新值30,返回失败
boolean result2 = atomicInteger.compareAndSet(10, 30);
System.out.println("第二次CAS:" + result2 + ",当前值:" + atomicInteger.get()); // false,20
}
}
这段代码很直观:第一次 CAS 时,内存值和预期值一致,修改成功;第二次预期值和内存值不匹配,修改失败。
2.2 CAS执行流程(mermaid图直观展示)
是
否
线程读取内存地址V的值,记录为预期值A
线程计算目标新值B
比较内存V的值是否等于A?
将内存V的值修改为B,操作成功
放弃修改或重新读取A,进入重试
2.3 底层源码窥探(AtomicInteger的getAndIncrement)
很多人面试时会被追问"AtomicInteger的自增为什么是线程安全的?",答案就是 CAS。来看 AtomicInteger 中 getAndIncrement 方法的核心源码(JDK8):
java
public final int getAndIncrement() {
// unsafe是Java提供的底层工具类,直接操作内存
// getAndAddInt的四个参数:内存地址、偏移量、预期值增量、实际增量
return unsafe.getAndAddInt(this, valueOffset, 0, 1);
}
// 底层CAS核心逻辑(简化版)
public final int getAndAddInt(Object o, long offset, int expected, int delta) {
int v;
// 循环重试:直到CAS成功
do {
// 读取内存中当前值v(对应预期值A)
v = getIntVolatile(o, offset);
// 比较并交换:如果内存值等于v,就修改为v+delta
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
关键在于 do-while 循环:如果 CAS 失败,就重新读取内存值,再次尝试,直到成功------这就是我们常说的"自旋"。

三、CAS的优缺点(面试必问,别只说一半)
CAS 作为无锁机制,既有明显优势,也有不可忽视的缺点,面试时要全面回答,避免只说优点踩坑。
3.1 优点
- 无锁开销小:无需加锁、释放锁,避免了 synchronized 锁带来的上下文切换、线程阻塞等开销,并发性能更高(适用于低冲突场景)。
- 原子性保证:基于 CPU 原子指令实现,能保证操作的原子性,无需额外加锁。
- 实现简单:核心逻辑就是"比较-交换",代码实现简洁,易于理解和使用。
3.2 缺点
- 自旋开销大:如果并发冲突严重,线程会一直自旋重试,占用 CPU 资源(比如大量线程同时修改一个变量,会导致很多线程反复重试,CPU 使用率飙升)。
- 只能保证单个变量原子操作:CAS 只能对单个变量进行原子修改,无法保证多个变量操作的原子性(比如同时修改两个变量,无法用 CAS 直接实现)。
- 存在 ABA 问题:这是 CAS 最经典的问题,也是面试官最爱追问的点,下面单独拆解。
四、ABA问题(核心重点,面试高频)

4.1 什么是ABA问题?
简单来说:线程1读取变量值为 A,线程2将变量修改为 B,然后又修改回 A;此时线程1进行 CAS 操作,发现内存值还是 A,就认为变量没被修改过,执行交换操作------但实际上变量已经被修改过(A→B→A),这就是 ABA 问题。
举个例子:你有100元(A),准备转给朋友(CAS 修改为0);此时另一个人先给你转了100元(B),又马上转走100元(A);你执行 CAS 时,发现余额还是100元,就以为没被操作过,顺利转账------但实际上余额已经被变动过,虽然最终结果正确,但可能隐藏潜在风险(比如对账异常)。
4.2 ABA问题的危害
大多数场景下,ABA 问题不会影响最终结果(比如简单的自增、自减),但在有状态的场景中会出现问题:比如链表节点的插入/删除,可能导致链表结构错乱,出现死循环或数据丢失。
4.3 ABA问题的3种解决方案
方案1:使用版本号(最常用)
核心思路:给变量增加一个版本号,每次修改变量时,不仅修改值,还会让版本号自增;CAS 操作时,不仅比较变量值,还比较版本号,只有"值相等且版本号相等",才执行修改。
比如:初始状态(值=A,版本=1)→ 线程2修改为 B(版本=2)→ 线程2再修改为 A(版本=3);线程1 CAS 时,预期值A、预期版本1,而实际版本3,因此修改失败,避免 ABA 问题。
方案2:使用 AtomicStampedReference(Java 提供的现成工具类)
Java 中已经封装了带版本号的 CAS 实现------AtomicStampedReference,直接使用即可,无需自己实现版本号管理。
示例代码:
java
public class ABADemo {
public static void main(String[] args) {
// 初始化:值为A,版本号为1
AtomicStampedReference<String> asr = new AtomicStampedReference<>("A", 1);
int oldStamp = asr.getStamp(); // 获取当前版本号1
String oldValue = asr.getReference(); // 获取当前值A
// 线程2模拟ABA操作
new Thread(() -> {
// 第一次修改:A→B,版本号1→2
asr.compareAndSet("A", "B", 1, 2);
// 第二次修改:B→A,版本号2→3
asr.compareAndSet("B", "A", 2, 3);
}).start();
// 线程1执行CAS,预期值A、版本号1
boolean result = asr.compareAndSet(oldValue, "C", oldStamp, oldStamp + 1);
System.out.println("CAS是否成功:" + result); // false
System.out.println("当前值:" + asr.getReference()); // A
System.out.println("当前版本号:" + asr.getStamp()); // 3
}
}
方案3:禁止值回滚(场景受限)
如果业务场景允许,可规定变量值只能单向修改(比如只能递增、只能从 null 改为非 null),禁止值回滚,从根源上避免 ABA 问题。但这种方案适用性有限,只适合特定业务场景。
五、面试官追问环节
这部分是重点!比纯背八股文有用,提前准备好,面试时直接加分,整理了3个高频追问,附标准答案:
追问1:CAS 和 synchronized 的区别?
核心区别:锁机制不同,适用场景不同。
- 锁类型:synchronized 是悲观锁,默认认为会发生并发冲突,直接加锁阻塞线程;CAS 是乐观锁,默认认为不会发生冲突,不阻塞线程,失败后自旋重试。
- 开销:synchronized 有锁的上下文切换、线程阻塞开销;CAS 无锁开销,但冲突严重时自旋会占用 CPU。
- 适用场景:synchronized 适用于高冲突、多变量操作场景;CAS 适用于低冲突、单变量操作场景(如 Atomic 系列工具类)。
追问2:AtomicInteger 为什么不用 synchronized 而用 CAS?
因为 AtomicInteger 主要用于单变量的原子操作(如自增、自减),用 CAS 无需加锁,能减少锁开销,提升并发性能;而 synchronized 加锁会导致线程阻塞,在低冲突场景下,性能不如 CAS。
另外,synchronized 是重量级锁(JDK1.8 虽有优化,但仍有阻塞开销),而 CAS 基于 CPU 原子指令,轻量级,更适合简单的原子操作场景。
追问3:ABA问题的本质是什么?怎么避免?
本质:CAS 只关注"值是否一致",不关注"值是否被修改过",无法区分"值从未修改"和"值修改后回滚"两种情况。
避免方案(优先选前两种):
- 使用版本号机制,给变量增加版本标识,CAS 同时比较值和版本号。
- 使用 Java 提供的 AtomicStampedReference 工具类,封装了版本号管理。
- 业务上禁止变量值回滚,从根源上杜绝 ABA 场景。
六、总结
CAS 是无锁并发编程的核心,底层依赖 CPU 原子指令,核心是"比较并交换",也是 Atomic 系列工具类的底层实现。它的优点是无锁开销小、原子性强,缺点是自旋开销大、只能保证单变量原子操作、存在 ABA 问题。
面试时,不仅要讲清 CAS 原理,还要能说出优缺点和 ABA 问题的解决方案,再结合面试官追问的知识点,就能轻松拿下这个考点。