☰
一文详解CAS
2026/10/8 7:07:45 网站建设 项目流程

CAS 详解 + 场景示例

CAS:Compare And Swap,比较并交换,是无锁乐观锁的原子操作,CPU 硬件指令支持,不是 Java 独有的概念,Java 里Unsafe类提供 CAS 底层方法,AtomicInteger等原子类就是基于 CAS 实现。

一、核心原理

CAS 包含 3 个参数:V(内存地址上的实际值),A(预期旧值),B(要更新的新值)逻辑:

  1. 比较:判断内存当前值V是否 == 预期旧值A
  2. 如果相等 → 说明期间没有被别人修改,把 V 更新成 B,返回 true
  3. 如果不相等 → 说明已经被其他线程改过,更新失败,返回 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。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询