原子类与 CAS

上一章讲锁——ReentrantLock/synchronized 都是”悲观策略”:先加锁再操作,假设一定有冲突。但很多场景冲突并不频繁——悲观锁的开销主要花在”加锁/解锁”本身,而不是冲突。能不能”不加锁”也保证安全?

答案就是 CAS(Compare-And-Swap) ——一种”乐观策略”:先操作,提交时检查有没有冲突,冲突就重试。Java 的 java.util.concurrent.atomic 包提供了基于 CAS 的原子类——AtomicIntegerAtomicLongAtomicReference 等。它们是无锁编程的基石,也是 ConcurrentHashMapLongAdderAtomicStampedReference 等高级并发工具的底层。

一、CAS 原理

1.1 CAS 是什么

CAS(Compare-And-Swap) 是一条硬件支持的原子指令——CPU 直接保证”比较并交换”不可被打断。

伪代码:

boolean CAS(memory, expected, newValue) {
    if (memory == expected) {     // 比较
        memory = newValue;        // 交换
        return true;
    }
    return false;                  // 失败
}

整条指令由 CPU 保证原子。在 x86 上是 lock cmpxchg 指令,在 ARM 上是 LDREX/STREX 指令对。

1.2 CAS 的工作流程

AtomicInteger.incrementAndGet() 的实现思路:

1. 读当前值 V = value
2. 计算新值 N = V + 1
3. CAS(value, V, N)
   - 如果 value == V,写入 N,返回 true → 成功
   - 否则返回 false → 被别人改过,重试
4. 失败就回到第 1 步

整个”读-算-比-写”的过程中没有锁——只有最后一步 CAS 是原子的。如果中间被别人改过,CAS 失败,循环重试。这叫自旋(spin)

1.3 Java 里的 CAS:Unsafe 与 VarHandle

Java 通过 sun.misc.Unsafe(JDK 9+ 推荐 VarHandle)调用 CAS:

// Unsafe API
public final native boolean compareAndSwapInt(Object o, long offset,
                                              int expected, int x);

// VarHandle(JDK 9+)
VarHandle vh = MethodHandles.lookup().findVarHandle(...)
vh.compareAndSet(instance, expected, newValue);

AtomicInteger.compareAndSet 内部就是调用 Unsafe.compareAndSwapInt

1.4 CAS 的三大问题

问题描述解决
ABA值从 A→B→A,CAS 认为没变过AtomicStampedReference 加版本号
自旋开销高争用下 CAS 反复失败重试,CPU 飙升LongAdder 分段、限制重试次数
只能单变量一次 CAS 只能改一个变量AtomicReference 包装对象,或用锁

二、原子类家族

java.util.concurrent.atomic 包提供了一系列原子类,分为几类:

2.1 基本类型

  • AtomicBoolean
  • AtomicInteger——常用,incrementAndGet/getAndIncrement/addAndGet/compareAndSet
  • AtomicLong
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();   // ++counter,返回新值
counter.getAndIncrement();   // counter++,返回旧值
counter.addAndGet(5);        // counter += 5
counter.compareAndSet(0, 1); // 如果是 0 才设为 1,返回是否成功
counter.updateAndGet(x -> x * 2);   // Java 8+,函数式更新
counter.getAndUpdate(x -> x + 1);
counter.accumulateAndGet(10, Integer::sum);   // 累加

2.2 引用类型

  • AtomicReference<V>——引用类型原子更新
  • AtomicStampedReference<V>——带版本号,解决 ABA
  • AtomicMarkableReference<V>——带布尔标记
AtomicReference<String> ref = new AtomicReference<>("init");
ref.set("new");
String old = ref.getAndSet("newer");
boolean success = ref.compareAndSet("newer", "newest");

2.3 数组类型

  • AtomicIntegerArray/AtomicLongArray/AtomicReferenceArray
AtomicIntegerArray arr = new AtomicIntegerArray(10);
arr.incrementAndGet(0);
arr.compareAndSet(0, 1, 5);

2.4 字段更新器

  • AtomicIntegerFieldUpdater<T>
  • AtomicLongFieldUpdater<T>
  • AtomicReferenceFieldUpdater<T,V>

指定类的指定 volatile 字段变成原子更新——不需要把字段包装成 AtomicInteger,节省内存。

class Person {
    volatile int age;   // 必须 volatile
}

AtomicIntegerFieldUpdater<Person> ageUpdater =
    AtomicIntegerFieldUpdater.newUpdater(Person.class, "age");

Person p = new Person();
ageUpdater.incrementAndGet(p);

限制:字段必须是 volatile、非 private、非 static

2.5 累加器(Java 8+)

  • LongAdder/DoubleAdder——高并发累加专用
  • LongAccumulator/DoubleAccumulator——自定义累加函数

LongAdderAtomicLong 的高并发替代——后面会详讲。

三、ABA 问题

3.1 什么是 ABA

CAS 只比较”值是否变化”,不关心”变化过程”。如果值从 A→B→A,CAS 认为没变过——但其实中间发生过变化。

典型场景——栈的无锁实现:

栈顶 A → B → C

线程 1 准备 pop A:读 top = A,准备 CAS(top, A, B)
线程 2 在线程 1 CAS 前,pop A、pop B、push A:
    栈变成 A → C(B 被弹走了,A 被重新压回)

线程 1 CAS(top, A, B):成功!top 变成 B
但 B 已经被弹出栈了,现在栈顶指向一个不再栈里的节点——链表损坏!

ABA 的危害:数据结构被破坏,但 CAS 还以为成功了

3.2 AtomicStampedReference 解决 ABA

AtomicStampedReference 给每个值配一个版本号——每次修改版本号递增,CAS 同时比较值和版本号。

AtomicStampedReference<Integer> ref =
    new AtomicStampedReference<>(100, 0);   // 初始值 100,版本 0

int[] stampHolder = new int[1];
Integer current = ref.get(stampHolder);
int stamp = stampHolder[0];   // 读出当前版本

// 别的线程把 100 改成 200 再改回 100,但 stamp 变了
ref.compareAndSet(100, 200, 0, 1);   // 100→200, stamp 0→1
ref.compareAndSet(200, 100, 1, 2);   // 200→100, stamp 1→2

// 我用旧 stamp CAS:失败,因为 stamp 已经是 2 不是 0
boolean ok = ref.compareAndSet(100, 999, stamp, stamp + 1);
System.out.println(ok);   // false

AtomicMarkableReference 类似,但只用一个 boolean 标记——适合”是否被删除过”这种二元判断。

3.3 ABA 实际影响

ABA 只在某些场景下是问题

  • 单纯计数(如 AtomicInteger)——ABA 没影响,我们只关心最终值。
  • 链式数据结构(如栈、队列的节点复用)——ABA 会破坏结构,必须用版本号。

四、LongAdder vs AtomicLong

这是高并发计数器设计的经典对比。

4.1 AtomicLong 的瓶颈

AtomicLong 所有线程对同一个 volatile long value 做 CAS——高并发下:

  • CAS 频繁失败,自旋重试,CPU 飙升。
  • 同一缓存行反复 Invalidate——缓存一致性协议开销大(虚假共享)。

4.2 LongAdder 的分段思想

LongAdder 把一个计数器拆成多个 Cell(base + 若干 Cell),每个线程优先 CAS 自己的 Cell,最后 sum() 时把所有 Cell 加起来。

AtomicLong:        value ←── CAS ── 线程 1/2/3/4/.../100   (全部争用一处)

LongAdder:         base      ← CAS(无 Cell 时)
                   Cell[0]   ← CAS ── 线程 1
                   Cell[1]   ← CAS ── 线程 2
                   Cell[2]   ← CAS ── 线程 3
                   ...
                   sum() = base + ΣCell[i]

争用被分散到多个 Cell——每个 Cell 的 CAS 失败概率大幅降低。这就是”分离状态”思想的胜利——把共享状态分离成多个独立状态,避免争用。

4.3 性能对比

场景AtomicLongLongAdder
低并发(<4 线程)接近接近
中高并发快 2-5 倍
极高并发极慢(CAS 风暴)快 10+ 倍
sum() 调用O(1)O(n),遍历所有 Cell
内存大(多个 Cell)

适用

  • LongAdder 适合”高频写、低频读”的计数场景——统计接口 QPS、采集指标。
  • AtomicLong 适合”读写均衡”或”需要序列号/全局唯一 id”——因为 LongAdder.sum() 不保证强一致性(读期间可能有 Cell 在更新)。

Cell 类还用了 @Contended 注解——避免虚假共享,进一步加速。

4.4 LongAccumulator

LongAccumulatorLongAdder 的泛化——可指定累加函数:

LongAccumulator max = new LongAccumulator(Long::max, Long.MIN_VALUE);
max.accumulate(10);
max.accumulate(20);
max.get();   // 20

五、非阻塞算法简介

非阻塞算法(Non-blocking Algorithm)——线程失败或挂起不会导致其他线程无法推进。基于 CAS 的算法是非阻塞的。

5.1 阻塞 vs 非阻塞 vs 无锁

类别描述例子
阻塞线程拿不到锁就挂起synchronizedReentrantLock.lock
非阻塞失败不挂起,可重试CAS 自旋
无锁(lock-free)至少一个线程能进展AtomicIntegerConcurrentLinkedQueue
无等待(wait-free)每个线程都保证有限步内完成极少,实现复杂

5.2 经典非阻塞算法:Treibor Stack

无锁栈——用 AtomicReference<Node> 保存栈顶,push/pop 都用 CAS:

class ConcurrentStack<T> {
    AtomicReference<Node<T>> top = new AtomicReference<>();

    public void push(T value) {
        Node<T> newNode = new Node<>(value);
        Node<T> oldTop;
        do {
            oldTop = top.get();
            newNode.next = oldTop;
        } while (!top.compareAndSet(oldTop, newNode));
    }

    public T pop() {
        Node<T> oldTop, newTop;
        do {
            oldTop = top.get();
            if (oldTop == null) return null;
            newTop = oldTop.next;
        } while (!top.compareAndSet(oldTop, newTop));
        return oldTop.value;
    }

    static class Node<T> {
        T value;
        Node<T> next;
        Node(T v) { value = v; }
    }
}

这个栈在 push/pop 时用 CAS——失败就自旋重试。多个线程同时操作时,至少一个能成功——是无锁的。

注意:Treibor Stack 的 pop 存在 ABA 问题——节点被弹出后又重新 push,CAS 仍可能成功,但语义已被破坏。生产级实现需要用 AtomicStampedReference 或 GC 来避免(GC 保证弹出的节点不会被复用)。

ConcurrentLinkedQueue(Michael-Scott 算法)就是无锁队列的经典实现,我们下一章会讲。

六、实战:原子类与 CAS 演示

下面这个例子对比 synchronized/AtomicInteger/LongAdder 的性能,演示 ABA 问题和 AtomicStampedReference 的解决,以及字段更新器的用法。

Java · 在线运行

观察重点

  • 性能排序LongAdder > AtomicInteger > synchronized(高并发下)。
  • ABA 演示:无版本号的 CAS 在 A→B→A 后仍能成功——埋下隐患。
  • AtomicStampedReference:用版本号能检测出中间的变化,CAS 失败。
  • AtomicIntegerFieldUpdater:让普通 volatile 字段获得原子更新能力,不用包装类。
  • 自旋锁:用 AtomicBoolean+CAS 实现——无锁但可能忙等。Thread.onSpinWait()(Java 9+)提示 CPU 自旋。

七、本章小结

概念核心要点
CAS硬件原子指令,比较并交换;Java 通过 Unsafe/VarHandle 调用
CAS 三大问题ABA、自旋开销、只能单变量
AtomicInteger/AtomicLong基本类型原子类,incrementAndGet/compareAndSet
AtomicReference引用类型原子更新
AtomicStampedReference加版本号解决 ABA
AtomicIntegerFieldUpdater让指定 volatile 字段原子更新,节省内存
ABA值 A→B→A 让 CAS 误判;链式结构致命,纯计数无害
LongAdder分段累加,高并发远超 AtomicLong
LongAdder 适用高频写低频读、统计计数
AtomicLong 适用序列号、读写均衡
非阻塞算法CAS 自旋,至少一个线程能进展
Thread.onSpinWait()Java 9+,提示 CPU 自旋优化

记忆口诀

  • CAS 是”乐观锁”——先干再查,失败重试,不加锁。
  • ABA 在计数无影响,在链式结构致命——栈/队列要用版本号。
  • LongAdder 是高并发计数之王——分段思想,分而治之。
  • AtomicLong 适合序列号——LongAdder.sum() 不强一致。
  • 字段更新器省内存——但字段必须 volatile 非 private。

结语:从锁到无锁

原子类与 CAS 是 Java 并发的”另一条腿”——它用硬件原子指令替代锁,在低争用场景下性能远胜锁。AtomicInteger 让计数器无锁安全,LongAdder 让高并发计数器飙升,AtomicStampedReference 解决 ABA 难题,CAS 自旋锁展示了无锁的可能。

但 CAS 也不是万能——高争用下自旋开销大,单变量限制存在。下一章我们看 并发集合——ConcurrentHashMap(CAS + synchronized 锁桶)、CopyOnWriteArrayList(写时复制)、ConcurrentLinkedQueue(无锁 CAS)、BlockingQueue 系列。它们把锁和 CAS 巧妙组合,提供线程安全的容器——是日常并发编程最常用的工具。