CAS 详解 + 场景示例
CAS:Compare And Swap,比较并交换,是无锁乐观锁的原子操作,CPU 硬件指令支持,不是 Java 独有的概念,Java 里
Unsafe类提供 CAS 底层方法,AtomicInteger等原子类就是基于 CAS 实现。
一、核心原理
CAS 包含 3 个参数:V(内存地址上的实际值),A(预期旧值),B(要更新的新值)逻辑:
- 比较:判断内存当前值
V是否 == 预期旧值A - 如果相等 → 说明期间没有被别人修改,把 V 更新成 B,返回 true
- 如果不相等 → 说明已经被其他线程改过,更新失败,返回 false,不做修改
一句话:
我认为现在的值是 A,如果确实还是 A,我就改成 B;
变了就放弃修改,重试。
⚠️ 重点:
CAS 是原子指令,一条 CPU 指令完成比较 + 交换,不会出现比较和赋值中间被线程切换打断的问题.
所以不需要 synchronized 重量级锁,属于乐观锁(假设并发冲突概率不高,不加锁,冲突了重试)。
二、Java 中 CAS 底层入口
sun.misc.Unsafe的 native 方法:
// var1:对象,var2:字段偏移量,var4:预期值,var5:新值 public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);AtomicInteger.getAndIncrement()自增底层就是:
// 伪代码 do { A = get(); // 拿到当前预期旧值A B = A + 1; // 新值B } while( ! compareAndSwapInt(this, offset, A, B) ); // CAS失败就循环重试三、CAS 的 3 大经典问题(面试高频)
1. ABA 问题
现象:
线程 1 读取值 A,线程 2 把 A 改成 B,再改回 A;
线程 1 执行 CAS 时发现还是 A,认为没被修改,成功更新,但中间发生过修改。
举例子:
- 初始值:
V = A - T1:读取 V=A,准备 CAS;此时时间片被抢走,暂停
- T2:把 V 从 A→B,再把 V 从 B→A
- T1 恢复,发现 V==A,CAS 成功。T1 感知不到中间发生过变化
解决方案:
版本号(时间戳)每次修改带上版本号,比较的时候同时比较【值 + 版本】。AtomicStampedReference就是存(值, stamp版本号)解决 ABA。
2. 循环自旋消耗 CPU
CAS 失败会循环重试(自旋),如果并发极高,大量线程一直 CAS 失败,无限循环,CPU 飙升。
优化:
限制自旋次数、退化为重量级锁(JDK 偏向锁 / 轻量级锁里的自适应自旋)
3. 只能保证一个变量原子性
CAS 一次只能操作一个内存变量。
如果要同时修改多个变量,CAS 无能为力,需要锁,或者用AtomicReference封装成对象。
四、场景示例
场景 1:计数器(AtomicInteger,最常用)
多线程累加统计,比如接口请求计数,不用synchronized
import java.util.concurrent.atomic.AtomicInteger; public class CasDemo { // 原子计数器,底层CAS static AtomicInteger count = new AtomicInteger(0); public static void add() { // count++ 原子自增 count.getAndIncrement(); } public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 5000; i++) add(); }); Thread t2 = new Thread(() -> { for (int i = 0; i < 5000; i++) add(); }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count.get()); // 结果一定=10000,线程安全 } }如果用普通
int count不加锁,大概率小于 10000.因为
count++是读 - 改 - 写三步,会丢失更新;而 CAS 原子操作保证线程安全。
场景 2:ABA 场景示例(AtomicStampedReference)
import java.util.concurrent.atomic.AtomicStampedReference; public class ABADemo { // 参数:初始值,初始版本号 static AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(100, 1); public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { int stamp = ref.getStamp(); // 获取版本号=1 Integer oldVal = ref.getReference(); // old=100 try { Thread.sleep(1000); // 休眠,让t2修改 } catch (InterruptedException e) {e.printStackTrace();} // CAS:预期值100,新版本+1;同时校验版本号 boolean ok = ref.compareAndSet(oldVal, 200, stamp, stamp+1); System.out.println("t1 CAS结果:" + ok); }); Thread t2 = new Thread(() -> { int stamp = ref.getStamp(); // 100 → 101,版本+1 ref.compareAndSet(100,101, stamp, stamp+1); // 101 → 100,版本再加1 ref.compareAndSet(101,100, ref.getStamp(), ref.getStamp()+1); }); t1.start(); t2.start(); } }运行结果:t1 CAS结果:false
虽然值变回 100,但是版本号已经变成 3,t1 持有的版本号还是 1,CAS 失败,成功规避 ABA。
场景 3:自己手写简易 CAS(模拟)
注意:
下面只是伪代码模拟,真实 CAS 必须 CPU 硬件指令,Java 代码无法实现真正原子性
// 模拟CAS逻辑 public boolean cas(int expect, int newValue){ if (this.value == expect) { this.value = newValue; return true; } return false; }这个模拟代码不是原子的!比较和赋值中间可以被线程打断,不是真正 CAS,仅用来理解逻辑。
五、CAS vs synchronized
| CAS (乐观锁) | synchronized (悲观锁) | |
|---|---|---|
| 锁思想 | 不加锁,假设冲突少,失败重试 | 认为会冲突,直接锁住 |
| 底层 | CPU 原子指令 | OS 互斥锁(JDK6 后有偏向 / 轻量锁,底层也用到 CAS) |
| 开销 | 无内核态切换;自旋高时 CPU 高 | 竞争大时阻塞,线程挂起唤醒,开销大 |
| 适用场景 | 并发冲突少 | 并发冲突激烈 |
JDK6 之后 synchronized 优化:偏向锁、轻量级锁底层就是用 CAS,竞争激烈膨胀成重量级锁。
六、业务上什么时候用 CAS?
✅ 适合:读多写少,并发冲突概率低
- 接口 QPS 计数器、埋点统计
- 简单状态标记:比如「0 = 未初始化,1 = 已初始化」,防止重复初始化(双重检查锁 DCL 也用到 CAS 思想)
- 并发容器:
ConcurrentHashMap更新节点、ConcurrentLinkedQueue无锁队列底层大量 CAS
❌ 不适合:高并发写、大量线程争抢同一个变量,会大量自旋,CPU 打满,此时用 synchronized 或者锁更好。
总结
CAS 是比较并交换,硬件原子指令实现乐观锁;比较内存值和预期旧值,相等则更新,失败自旋重试;存在 ABA、CPU 自旋、只能单变量原子性三个问题,适合读多写少场景,Atomic 原子类底层依赖 CAS。