谈谈 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/
面试考察点
- 对并发基础概念的掌握: 是否理解什么是 “乐观锁”,以及它与 “悲观锁”(如 synchronized)的核心区别。这是理解 CAS 的前提。
- 对硬件级并发的理解深度: 不仅仅是知道 CAS 是 “比较并交换”,更要清楚它是如何利用 CPU 的原子指令(如 cmpxchg)在无锁的情况下保证线程安全的。这体现了候选人对 “语言特性” 之下 “硬件支撑” 的理解。
- 对 CAS 经典问题的认知: 是否了解 CAS 的典型缺陷,比如 ABA 问题、自旋开销、以及只能保证一个共享变量的原子操作。更重要的是,是否知道如何解决这些问题(比如 AtomicStampedReference)。
- 源码级别的熟悉程度(加分项): 是否阅读过
java.util.concurrent.atomic包下的原子类源码,或者AbstractQueuedSynchronizer(AQS)中如何使用 CAS 来实现同步状态的管理。 - 实际应用场景的理解: 能否说出在 JDK 中哪些地方用到了 CAS,比如并发包中的 AtomicInteger、ConcurrentHashMap 等。
核心答案
一句话回答: CAS(Compare and Swap,比较并交换)是一种乐观锁技术,它通过硬件层面的原子指令,在无锁的情况下实现对共享变量的线程安全更新。它的核心思想是:“我认为共享变量的当前值应该是 A,如果是,那我就把它改成 B;如果不是,就说明被别人改过了,那我就不修改,并告诉我修改失败。”
深度解析
1. 原理与机制
CAS 的操作包含三个操作数:
- 内存位置 V:也就是你要操作的共享变量的内存地址。
- 预期原值 A:你期望在执行操作前,这个变量应该是什么值。
- 新值 B:你想把这个变量设置成什么新值。
执行逻辑: CAS 指令会原子的执行以下两步:
- 比较 V 的值是否等于 A。
- 如果等于,则将 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 的变动。这在某些场景下(如无锁的链表操作)可能会引发数据一致性问题。
解决方案:
使用带有版本号/时间戳的原子引用,例如 AtomicStampedReference 或 AtomicMarkableReference。它们不仅比较值,还比较一个内部的状态戳,确保 “值” 和 “版本” 都一致才算成功。
误区二:认为 CAS 能随意替代所有锁
CAS 更适合于对单个共享变量的简单更新,比如计数器、状态标志。如果操作涉及多个变量的协同修改,或者复杂的业务逻辑,使用 synchronized 或 ReentrantLock 会让代码更简单、更安全。
最佳实践:
- 优先使用
java.util.concurrent.atomic包下的工具类,它们已经封装好了 CAS 操作,比如AtomicInteger,LongAdder(在超高并发下性能更好)。 - 在自旋(循环重试 CAS)时,可以加入一定的退避策略(如
Thread.yield()或Thread.sleep()),避免在竞争激烈时过度占用 CPU。
总结
一句话总结: CAS 是一种借助 CPU 原子指令实现的无锁、非阻塞的并发更新机制,它通过“比较-交换”的原子操作避免了线程上下文切换的开销,是 Java 并发包(JUC)的基石。理解它的原理、优缺点(尤其是 ABA 问题),是掌握高并发编程的关键一步。