谈谈 CAS 原理?
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。
浙公网安备 33010602011771号