【面试专栏|Java并发编程】CAS 核心原理,优缺点,ABA问题与解决方案


🍃 予枫个人主页
📚 个人专栏 : 《Java 从入门到起飞》《读研码农的干货日常》《Java 面试刷题指南

💻 Debug 这个世界,Return 更好的自己!


引言

家人们谁懂啊!Java并发面试里,CAS绝对是"常驻嘉宾",不管是初级还是中级面试官,必问一句"你说说CAS的核心原理"。很多人只记个"比较并交换"的表面,被追问ABA问题、优缺点时直接卡壳。今天就从底层原理、代码实战、面试追问三个维度,把CAS讲透,让你下次面试遇到它,能侃侃而谈!

文章目录

一、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 优点

  1. 无锁开销小:无需加锁、释放锁,避免了 synchronized 锁带来的上下文切换、线程阻塞等开销,并发性能更高(适用于低冲突场景)。
  2. 原子性保证:基于 CPU 原子指令实现,能保证操作的原子性,无需额外加锁。
  3. 实现简单:核心逻辑就是"比较-交换",代码实现简洁,易于理解和使用。

3.2 缺点

  1. 自旋开销大:如果并发冲突严重,线程会一直自旋重试,占用 CPU 资源(比如大量线程同时修改一个变量,会导致很多线程反复重试,CPU 使用率飙升)。
  2. 只能保证单个变量原子操作:CAS 只能对单个变量进行原子修改,无法保证多个变量操作的原子性(比如同时修改两个变量,无法用 CAS 直接实现)。
  3. 存在 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 的区别?

核心区别:锁机制不同,适用场景不同。

  1. 锁类型:synchronized 是悲观锁,默认认为会发生并发冲突,直接加锁阻塞线程;CAS 是乐观锁,默认认为不会发生冲突,不阻塞线程,失败后自旋重试。
  2. 开销:synchronized 有锁的上下文切换、线程阻塞开销;CAS 无锁开销,但冲突严重时自旋会占用 CPU。
  3. 适用场景:synchronized 适用于高冲突、多变量操作场景;CAS 适用于低冲突、单变量操作场景(如 Atomic 系列工具类)。

追问2:AtomicInteger 为什么不用 synchronized 而用 CAS?

因为 AtomicInteger 主要用于单变量的原子操作(如自增、自减),用 CAS 无需加锁,能减少锁开销,提升并发性能;而 synchronized 加锁会导致线程阻塞,在低冲突场景下,性能不如 CAS。

另外,synchronized 是重量级锁(JDK1.8 虽有优化,但仍有阻塞开销),而 CAS 基于 CPU 原子指令,轻量级,更适合简单的原子操作场景。

追问3:ABA问题的本质是什么?怎么避免?

本质:CAS 只关注"值是否一致",不关注"值是否被修改过",无法区分"值从未修改"和"值修改后回滚"两种情况。

避免方案(优先选前两种):

  1. 使用版本号机制,给变量增加版本标识,CAS 同时比较值和版本号。
  2. 使用 Java 提供的 AtomicStampedReference 工具类,封装了版本号管理。
  3. 业务上禁止变量值回滚,从根源上杜绝 ABA 场景。

六、总结

CAS 是无锁并发编程的核心,底层依赖 CPU 原子指令,核心是"比较并交换",也是 Atomic 系列工具类的底层实现。它的优点是无锁开销小、原子性强,缺点是自旋开销大、只能保证单变量原子操作、存在 ABA 问题。

面试时,不仅要讲清 CAS 原理,还要能说出优缺点和 ABA 问题的解决方案,再结合面试官追问的知识点,就能轻松拿下这个考点。

相关推荐
tHeya06II18 分钟前
涵盖 Cursor、Claude Code、Skills
java·服务器
kim_puppy20 分钟前
TCP的三次握手,四次挥手
java·网络·tcp
诗人不写诗20 分钟前
spring boot apm生态
java·数据库·spring boot
海参崴-21 分钟前
C++代码格式规范
java·前端·c++
better_liang1 小时前
每日Java面试场景题知识点之-Redisson热门使用场景
java·redis·微服务·分布式锁·redisson·分布式系统
2301_792674861 小时前
java学习 day26
java
so2F32hj21 小时前
拆解 OpenHands(14)--- Microagents
java·开发语言
明灯伴古佛1 小时前
面试:什么是可重入性?为什么 synchronized 是可重入锁?
java·jvm·面试
卓怡学长1 小时前
m307自习室预订座位管理分析与实现
java·spring boot·spring
Arya_aa1 小时前
生猪养殖溯源系统前期准备与SpringBoot框架
java·spring boot