云计算百科
云计算领域专业知识百科平台

Java并发编程——CAS

Java并发编程基石:深入理解CAS(Compare-And-Swap)

在Java并发编程中,如果你听过下面这些类:

  • AtomicInteger / AtomicLong / AtomicReference
  • ConcurrentHashMap(1.8+的部分实现)
  • LongAdder / Striped64系列
  • AQS中的状态更新

那么你其实已经大量间接使用过 CAS 了。

但很多人只是知道“CAS是无锁的”“比synchronized轻量”,却不清楚它到底是怎么实现的、有什么隐患、什么时候该用、什么时候坚决不能用。

这篇文章带你从原理 → 源码 → 优缺点 → 典型问题 → 工程实践 完整梳理一遍CAS。

一、CAS到底是什么?

CAS 全称:Compare And Swap(比较并交换)

核心思想只有一句话:

“我认为当前值应该是A,如果确实是A,就把它改为B;如果不是A,说明别人动过了,我不做修改(或者重试)。”

用伪代码表示就是:

boolean cas(地址V, 期望值A, 新值B) {
if (内存位置V的值 == A) {
V的值改为B
return true;
} else {
return false;
}
}

关键点:上面整个“比较 → 修改”是一个原子操作,由CPU硬件指令直接保证(cmpxchg / cmpXchg / LL-SC 等)。

Java中并没有直接暴露这条指令,而是通过 sun.misc.Unsafe(后改为jdk.internal.misc.Unsafe)来调用。

二、Java中CAS是怎么实现的?

以AtomicInteger为例,看最核心的incrementAndGet()方法:

public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

再往下追getAndAddInt(简化版):此处的compareAndSwapInt方法会重新获取obj 在 offset 位置的值(即新v),与之前先获取的v值进行比较,相同时才会返回为true,否则为false。 注意

  • incrementAndGet并不会修改内存值,它只是调用了 getAndAddInt() 并在结果上加1,然后把最终的新值返回给调用者。
  • 真正负责尝试修改内存(并且只有在 CAS 成功时才真正修改)的代码是 compareAndSwapInt

public final int getAndAddInt(Object obj, long offset, int delta) {
int v;
do {
v = getIntVolatile(obj, offset); // 读最新值
} while (!compareAndSwapInt(obj, offset, v, v + delta)); // CAS失败就重试
return v;
}

可以看到典型的自旋 + CAS 模式:

  • 先读取当前值(volatile读,保证可见性)
  • 计算新值
  • 用CAS尝试把“旧值→新值”写回去
  • 失败就回到第1步重新读(自旋)
  • 这种模式也被称为乐观锁(先干,冲突了再回滚重来)。

    而synchronized和ReentrantLock属于悲观锁(先上锁再干)。

    三、CAS的优缺点对比

    维度CAS(乐观锁)synchronized / ReentrantLock(悲观锁)
    是否阻塞 不阻塞(自旋) 会阻塞
    高竞争场景性能 可能退化严重(大量空自旋) 相对稳定(线程进入阻塞队列)
    低竞争场景性能 极高(几乎无上下文切换) 较重(至少有一次加锁/解锁开销)
    代码复杂度 相对复杂(需要自己处理重试逻辑) 简单
    是否可重入 本身无“锁”概念 支持重入
    是否支持条件等待 不支持 支持(wait/notify、Condition)

    一句话总结适用场景:

    • 读多写少、低冲突 → CAS 通常完胜
    • 写多、冲突激烈 → synchronized / Lock 往往更稳定

    四、最经典的问题:ABA问题

    假设我们有一个栈顶指针(用AtomicReference实现),当前是:

    A → B → C

    head

    线程1想做 pop 操作:希望 head 从 A 变成 B

    正常流程:

  • 读到 head = A
  • CAS(head, A, B) → 成功
  • 但真实世界可能发生下面这件事:

    线程1 读到 head = A(准备CAS) ↓ 线程2 pop 了 A,head 变成 B 线程3 push 一个新的 A,head 又变回 A ↓ 线程1 执行 CAS(head, A, B) → 成功!

    但这其实是错误的! 因为此时的 A 已经不是原来的 A 了(可能是新的对象,版本不同),会导致数据结构损坏。

    ABA问题本质:只比较“值”是否相同,而没有比较“版本”是否相同。

    业界主流解决方案

  • 加版本号(最常见)

    AtomicStampedReference / AtomicMarkableReference

    // 带版本戳的引用
    AtomicStampedReference<Node> head = new AtomicStampedReference<>(nodeA, 0);

    // CAS时同时检查版本
    head.compareAndSet(oldRef, newRef, oldStamp, oldStamp + 1);

  • 只关心是否“动过”而不关心具体值 → 用AtomicMarkableReference(只用一个boolean标记)

  • 五、常见的CAS使用

    场景推荐工具类备注
    普通int/long自增 AtomicInteger / AtomicLong 日常最常用
    高并发统计(Java8+) LongAdder / DoubleAdder 分段思想,性能远超Atomic*
    boolean标志位 AtomicBoolean
    引用类型 AtomicReference
    带版本的引用 AtomicStampedReference 解决ABA
    只关心是否动过 AtomicMarkableReference 轻量级ABA方案
    对象字段原子更新 AtomicIntegerFieldUpdater 等 不改业务代码,适合遗留系统
    ConcurrentHashMap计数 CounterCell + baseCount 内部大量使用LongAdder思想

    一个小建议:

    当并发量不是特别高(< 几百qps),直接用LongAdder几乎总是比AtomicLong更优。

    // 推荐写法(2024~2025主流)
    private final LongAdder counter = new LongAdder();

    public void incr() {
    counter.increment();
    }

    public long get() {
    return counter.sum();
    }

    六、总结一句话

    CAS是Java并发包(JUC)最核心的原语之一,它让“无锁编程”成为可能,但也带来了自旋开销、ABA隐患等工程代价。

    真正的高手不是“能不能用CAS”,而是“在什么场景下用哪种变体的CAS最合适”。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Java并发编程——CAS
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!