一文详解CAS

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 方法:

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。

相关推荐
鱼宵1 小时前
Spring AI 提示词模板:{变量} 参数化 + few-shot,一条提示词反复用
java·人工智能·spring·few-shot·提示词工程·springai
蜗牛互联网1 小时前
Python消费Responses SSE事件:增量文本、超时与取消
java·开发语言·人工智能·后端·python
wanderist.1 小时前
线性筛法详解:从筛质数到欧拉函数、Möbius 函数与约数函数
java·数据结构·算法
我不会起名字3222 小时前
Redis 入门(三):Spring Boot 整合 Redis,RedisTemplate 实战与序列化乱码
redis·spring·工具类·jedis·lettuce
Escalating_xu2 小时前
【C 语言】深入理解指针(5):sizeof、strlen、数组名语义与指针笔试题全解析
java·c语言·开发语言
HEJOO92 小时前
Linux 设备驱动系列——使用全局工作队列
java·linux·算法
谢亮_vipxieliang3 小时前
Go Channel 高级模式——从底层原理到扇出扇入实战
java·数据库·golang
Code_Solitude3 小时前
c语言:递归
java·c语言·算法
Nebula_g3 小时前
JavaSE加强:Stream流
java·开发语言·windows·stream·javase·流