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 方法:
java
// var1:对象,var2:字段偏移量,var4:预期值,var5:新值
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
AtomicInteger.getAndIncrement() 自增底层就是:
java
// 伪代码
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
java
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)
java
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 代码无法实现真正原子性
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。