谈谈 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)的核心区别。这是理解 CAS 的前提。
  2. 对硬件级并发的理解深度: 不仅仅是知道 CAS 是 “比较并交换”,更要清楚它是如何利用 CPU 的原子指令(如 cmpxchg)在无锁的情况下保证线程安全的。这体现了候选人对 “语言特性” 之下 “硬件支撑” 的理解。
  3. 对 CAS 经典问题的认知: 是否了解 CAS 的典型缺陷,比如 ABA 问题自旋开销、以及只能保证一个共享变量的原子操作。更重要的是,是否知道如何解决这些问题(比如 AtomicStampedReference)。
  4. 源码级别的熟悉程度(加分项): 是否阅读过 java.util.concurrent.atomic 包下的原子类源码,或者 AbstractQueuedSynchronizer(AQS)中如何使用 CAS 来实现同步状态的管理。
  5. 实际应用场景的理解: 能否说出在 JDK 中哪些地方用到了 CAS,比如并发包中的 AtomicInteger、ConcurrentHashMap 等。

核心答案

一句话回答: CAS(Compare and Swap,比较并交换)是一种乐观锁技术,它通过硬件层面的原子指令,在无锁的情况下实现对共享变量的线程安全更新。它的核心思想是:“我认为共享变量的当前值应该是 A,如果是,那我就把它改成 B;如果不是,就说明被别人改过了,那我就不修改,并告诉我修改失败。”

深度解析

1. 原理与机制

CAS 的操作包含三个操作数:

  • 内存位置 V:也就是你要操作的共享变量的内存地址。
  • 预期原值 A:你期望在执行操作前,这个变量应该是什么值。
  • 新值 B:你想把这个变量设置成什么新值。

执行逻辑: CAS 指令会原子的执行以下两步:

  1. 比较 V 的值是否等于 A。
  2. 如果等于,则将 V 的值更新为 B;否则,不进行任何操作。

整个 “比较 + 更新” 是一个原子操作,由 CPU 底层指令(例如 x86 架构下的 cmpxchg 指令)保证其不可分割性。

2. 代码示例:模拟 CAS 操作

虽然我们不能直接调用 CPU 指令,但 Java 的 sun.misc.Unsafe 类提供了对 CAS 的 native 方法支持。我们平时用的原子类底层就是通过它实现的。

import java.util.concurrent.atomic.AtomicInteger;

public class CASExample {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(10);
        
        // 尝试将值从 10 更新为 20
        boolean success1 = atomicInteger.compareAndSet(10, 20);
        System.out.println("第一次更新是否成功: " + success1); // 输出 true
        System.out.println("当前值: " + atomicInteger.get());   // 输出 20
        
        // 再次尝试将值从 10 更新为 30 (此时当前值是20)
        boolean success2 = atomicInteger.compareAndSet(10, 30);
        System.out.println("第二次更新是否成功: " + success2); // 输出 false
        System.out.println("当前值: " + atomicInteger.get());   // 输出 20 (值未变)
    }
}

在这个例子中,compareAndSet 方法就是 CAS 的体现。第二次操作因为预期原值 10 与内存中的当前值 20 不符,所以更新失败。

3. 对比分析:CAS vs 传统锁

特性CAS (乐观锁)synchronized (悲观锁)
核心思想认为并发冲突少,先更新,失败再重试。认为并发冲突多,先加锁,再操作。
线程阻塞无阻塞,线程不会进入阻塞状态,一直在用户态自旋。会阻塞,未抢到锁的线程会进入阻塞状态,涉及操作系统内核态切换。
性能低并发、短操作的场景下性能非常高。高并发、长操作的场景下能有效避免 CPU 空转。
风险ABA 问题、自旋 CPU 开销大、只能保证一个共享变量。死锁、优先级反转、上下文切换开销。

4. 常见误区与最佳实践

误区一:忽略 ABA 问题

ABA 问题是 CAS 的一个经典陷阱。假设一个变量初始值为 A,线程 1 准备执行 CAS(A -> B),但在它读取 A 之后、执行 CAS 之前,线程 2 将值从 A 改为了 B,又改回了 A。此时线程 1 执行 CAS 时,发现内存值还是 A,就会认为变量没被修改过,于是 CAS 成功。但事实上,变量已经经历了一次 A->B->A 的变动。这在某些场景下(如无锁的链表操作)可能会引发数据一致性问题。

解决方案:

使用带有版本号/时间戳的原子引用,例如 AtomicStampedReferenceAtomicMarkableReference。它们不仅比较值,还比较一个内部的状态戳,确保 “值” 和 “版本” 都一致才算成功。

误区二:认为 CAS 能随意替代所有锁

CAS 更适合于对单个共享变量的简单更新,比如计数器、状态标志。如果操作涉及多个变量的协同修改,或者复杂的业务逻辑,使用 synchronized 或 ReentrantLock 会让代码更简单、更安全。

最佳实践:

  • 优先使用 java.util.concurrent.atomic 包下的工具类,它们已经封装好了 CAS 操作,比如 AtomicIntegerLongAdder(在超高并发下性能更好)。
  • 在自旋(循环重试 CAS)时,可以加入一定的退避策略(如 Thread.yield()Thread.sleep()),避免在竞争激烈时过度占用 CPU。

总结

一句话总结: CAS 是一种借助 CPU 原子指令实现的无锁、非阻塞的并发更新机制,它通过“比较-交换”的原子操作避免了线程上下文切换的开销,是 Java 并发包(JUC)的基石。理解它的原理、优缺点(尤其是 ABA 问题),是掌握高并发编程的关键一步。