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

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

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

  • 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于 Spring AI + Spring Boot3.x + JDK 21...点击查看;
  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot3.x + JDK 17...点击查看项目介绍; 演示链接: http://116.62.199.48:7070/;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/

面试考察点

面试官提出这个问题,通常旨在考察以下几个核心层面:

  1. 对并发编程基础的掌握:是否理解除了 synchronized 等悲观锁之外,另一种基于乐观锁的核心无锁编程思想。
  2. 对 JUC 原子类底层原理的理解:是否能解释 AtomicIntegerAtomicReference 等类线程安全的根源。
  3. 深入计算机系统原理的能力:是否了解 CAS 是 CPU 级别的原子指令,而不仅仅是 Java 层面的一个概念。
  4. 批判性思维和实际问题排查能力:是否清楚 CAS 的局限性(如 ABA 问题)及其解决方案,这反映了候选人是否有过实际的高并发开发或问题排查经验。

核心答案

CAS 的全称是 Compare-And-Swap(比较并交换),它是一种用于实现多线程同步的原子操作。其核心思想是:我认为变量 V 的值应该是 A,如果是,那我就把它改成 B;如果不是 A,说明它已经被其他线程修改过了,那我就不修改,通常会选择重试或放弃。

CAS 操作是无锁(Lock-Free) 算法的基础,它避免了使用重量级锁(如 synchronized)带来的线程阻塞和上下文切换开销,因此在冲突不激烈的场景下性能很高。

CAS 主要存在以下问题:

  1. ABA 问题:一个值从 A 变成了 B,又变回了 A,CAS 检查时会认为它“没有被修改过”,从而可能导致逻辑错误。
  2. 自旋(循环)时间长导致 CPU 开销大:在高并发场景下,如果多个线程反复竞争更新同一变量,失败的线程会不断循环重试(自旋),消耗大量 CPU 资源。
  3. 只能保证一个共享变量的原子操作:一个 CAS 操作只能针对一个变量。对多个变量的操作,无法保证其原子性,需要借助其他手段(如锁或 AtomicReference 封装对象)。

深度解析

原理/机制:CAS 如何工作?

CAS 的逻辑可以用以下伪代码表示:

// 伪代码,展示CAS语义
public class SimulatedCAS {
    private int value;

    public synchronized int compareAndSwap(int expectedValue, int newValue) {
        int oldValue = this.value;
        if (oldValue == expectedValue) { // 比较
            this.value = newValue;      // 交换
        }
        return oldValue; // 总是返回旧值
    }
}

在实际硬件层面,CAS 是由 CPU 提供的一条原子指令(在 x86 架构上是 cmpxchg 指令)。这条指令在执行过程中会锁定缓存行或总线,确保 “比较” 和 “交换” 这两个动作作为一个不可分割的整体完成。

在 Java 中,我们通过 sun.misc.Unsafe 类(在 JDK 9 之后,部分功能被 VarHandle API 替代)提供的本地方法(如 compareAndSwapInt, compareAndSwapObject)来调用这条 CPU 指令。JUC 包下的原子类(如 AtomicInteger)内部都封装了这些操作。

代码示例:ABA 问题演示

import java.util.concurrent.atomic.AtomicInteger;

public class ABAProblemDemo {
    private static AtomicInteger atomicInt = new AtomicInteger(100);

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            // 线程1:期望值是100,想改成101
            boolean success1 = atomicInt.compareAndSet(100, 101);
            System.out.println(“线程1 CAS 100->101: ” + success1);
            // 再改回100
            boolean success2 = atomicInt.compareAndSet(101, 100);
            System.out.println(“线程1 CAS 101->100: ” + success2);
        });

        Thread t2 = new Thread(() -> {
            try {
                // 线程2先睡一会儿,确保线程1完成了一次ABA操作
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 线程2:期望值也是100,想改成102。它‘看到’的值是100,但不知道中间发生过100->101->100的变化。
            boolean success = atomicInt.compareAndSet(100, 102);
            System.out.println(“线程2 CAS 100->102: ” + success + “, 当前值: ” + atomicInt.get());
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}

输出可能为:

线程1 CAS 100->101: true
线程1 CAS 101->100: true
线程2 CAS 100->102: true, 当前值: 102

对于线程 2 来说,虽然 CAS 成功了,但此 “100” 已非彼 “100”。如果业务逻辑依赖 “值从未被修改” 这一假设,就会出问题。例如,在无锁栈的实现中,A->B->C 的栈顶被两个 pop 操作以 ABA 的方式错误地修改,可能导致数据丢失或内存泄漏。

对比分析与解决方案

  • ABA 问题的解决:使用 带版本号的原子引用 AtomicStampedReferenceAtomicMarkableReference。它们在比较时不仅比较引用值,还比较一个整型的 “戳记(Stamp)” 或布尔型的 “标记(Mark)”,类似于乐观锁的版本号机制。

    AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(100, 0);
    int stamp = ref.getStamp();
    ref.compareAndSet(100, 101, stamp, stamp + 1); // 更新值和版本号
    
  • 自旋开销的权衡:CAS 适合低至中度竞争的场景。如果竞争非常激烈,大量线程自旋,性能会急剧下降,此时传统的悲观锁(如 synchronized 在 JDK 6 之后优化得很好)或更高级的并发结构(如 LongAdder 可能是更好的选择。LongAdder 采用 “分段 CAS” 思想,将热点数据分离,非常适合高并发统计场景。

最佳实践与常见误区

  • 最佳实践
    1. 优先使用 JUC 原子类:如 AtomicIntegerAtomicLongFieldUpdater,而不是自己通过 Unsafe 操作。
    2. 明确适用场景:CAS 适用于读多写少、冲突概率不高的计数器、状态标志位等场景。
    3. 考虑替代方案:在超高并发写入场景下,评估 LongAddersynchronized
  • 常见误区
    1. 误解为万能银弹:认为无锁就一定比有锁快,忽略了高竞争下自旋的 CPU 开销。
    2. 忽视 ABA 问题:在不适合的场景下使用普通原子类,导致隐蔽的 Bug。
    3. 滥用复杂无锁算法:无锁数据结构(如队列)设计极其复杂,除非必要,优先使用 ConcurrentLinkedQueue 等成熟库。

总结

CAS 是一种基于 CPU 指令的乐观锁原子操作,它是 Java 中许多无锁并发工具的性能基石,但在享受其高性能优势时,必须警惕 ABA 问题 和高竞争下的 CPU 空转开销