【面试专栏|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 问题的解决方案,再结合面试官追问的知识点,就能轻松拿下这个考点。

相关推荐
老前端的功夫2 小时前
【Java从入门到入土】06:String的72变:从字符串拼接到底层优化
java·开发语言
又是忙碌的一天2 小时前
Java 面向对象三大特性:封装、继承、多态深度解析
java·前端·python
隔壁小邓2 小时前
在Java中实现优雅的CQRS架构
java·开发语言·架构
河边小咸鱼2 小时前
pdd校招实习生内推【实时更新链接】2027届实习、2026届春招
java·c++·golang
zzb15802 小时前
Agent学习-Reflection框架
java·人工智能·python·学习·ai
Holen&&Beer2 小时前
Spring-Profile与部署说明
java·后端·spring
棉花糖超人2 小时前
【操作系统】三、线程
java·开发语言·操作系统
liuyao_xianhui3 小时前
优选算法_判断字符是否唯一_C++
java·开发语言·数据结构·c++·算法·链表
代码雕刻家3 小时前
3.4.Maven-idea集成-导入Maven项目
java·maven·intellij-idea