跳到正文
Elaine Blog
返回

CAS、ABA 与 Atomic 原子类

并发编程

CAS 适合对单个状态执行“值仍是我看到的旧值时才更新”。它避免线程阻塞,但不是没有成本:高竞争会造成反复失败、 缓存一致性流量和不公平。多字段业务不变量通常仍需要锁或不可变状态整体替换。

Table of contents

Open Table of contents

运行 ABA 实验

下载带版本戳的 ABA 样例下载, 运行:

java -cp target/classes dev.elaine.concurrency.AtomicAbaSample

输出类似:

值=A,版本=2,旧观察能否更新=false

线程最初观察到 A@0。另一个执行流把它改为 B@1,再改回 A@2。只比较值时前后都是 A,这就是 ABA 问题; 同时比较版本戳后,旧观察无法覆盖新历史。

CAS 做了什么

**CAS(Compare-And-Set)**接收预期旧值和新值。内存中的值等于预期值时原子写入新值并返回成功,否则不修改并返回 失败。

var counter = new java.util.concurrent.atomic.AtomicInteger();
int next = counter.incrementAndGet();

AtomicInteger 把重试循环封装起来,并提供相应内存语义。不要用 volatile int++ 替代。

失败重试会消耗资源

典型 CAS 循环如下:

for (;;) {
    int current = counter.get();
    int next = current + 1;
    if (counter.compareAndSet(current, next)) return next;
    Thread.onSpinWait();
}

高竞争时,许多线程可能同时读取同一旧值,只有一个成功,其余继续占用 CPU。需要退避、分片或更高层结构。统计热点 可以使用 LongAdder 分散竞争,但 sum() 不是与所有并发更新形成单一原子快照,不适合余额等强一致值。

ABA 何时有害

只有历史变化影响正确性时,ABA 才是问题。无锁栈中节点 A 被弹出、复用后又放回,旧 CAS 可能误以为链表从未改变。 解决方案包括版本戳、不可复用标识、不可变节点,或使用已经验证的 JDK 并发容器。

AtomicStampedReference 同时比较引用和整数版本;AtomicMarkableReference 只携带布尔标记。不要为了“无锁”自行实现复杂 内存回收算法。

下一步

阅读AQS 与 ReentrantLock 源码主线,理解 CAS 失败后线程如何进入 等待队列并被唤醒。


分享这篇文章:

上一篇
synchronized 的实现、锁状态与适用边界
下一篇
AQS 与 ReentrantLock 源码主线