Lesson 21 · 并发编程

CAS 与原子操作:AtomicInteger 到 LongAdder

高级·#并发·#原子·#CAS

第 1 站

面试官:不加锁怎么保证原子性?

"多线程对共享变量做 i++,除了 synchronized 和 Lock,还有什么办法保证原子性?CAS 是什么?它有什么问题?AtomicLong 在高并发下为什么性能差,LongAdder 又是怎么解决的?" —— 面试官想考察的是你对无锁并发编程的理解深度。

在并发编程中,synchronized 和 Lock 是悲观策略——假定冲突一定会发生,所以先加锁再操作。而 CAS(Compare And Swap)是乐观策略——假定冲突很少发生,更新时检查是否被别人改过,没改过就原子更新,改过就重试。

基于 CAS 实现的原子类(AtomicIntegerAtomicLongAtomicReference 等)是 Java 并发工具箱中最高效的同步手段。它们无需加锁、无需线程上下文切换,在低竞争场景下性能远超锁方案。但 CAS 并非银弹——ABA 问题、自旋开销、只能保证单个变量原子性是它的三大局限。

本文从 CPU 指令级原理出发,逐层拆解 CAS → AtomicInteger → ABA 问题 → Unsafe → LongAdder,构建完整的无锁并发知识体系。

本文主线
CPU 指令 CMPXCHG → CAS 算法 → AtomicInteger 源码 → ABA 问题与解决方案 → Unsafe 与 VarHandle → LongAdder 分段优化
第 2 站

CAS 原理:从 CPU 指令到 Java API

CAS 的全称是 Compare And Swap(比较并交换),它是一条 CPU 原子指令(x86 上是 CMPXCHG),能在单条指令内完成"读-比较-写"三步操作,不会被其他线程打断。

CAS 的算法逻辑用伪代码表示极其简洁:

CAS 伪代码
// 原子操作:不可被中断
boolean CAS(int* addr, int expected, int newValue) {
    if (*addr == expected) {
        *addr = newValue;
        return true;   // 交换成功
    } else {
        return false;   // 值已被其他线程修改
    }
}

三个操作数:内存地址 V、预期旧值 E、新值 N。仅当 V 处的值等于 E 时,才将 V 更新为 N;否则不做任何操作。整个过程是原子的。

在 Java 中,CAS 通过 sun.misc.Unsafe 类暴露给开发者:

Unsafe CAS 使用示例
Unsafe unsafe = Unsafe.getUnsafe();
int offset = unsafe.objectFieldOffset(MyClass.class.getDeclaredField("count"));

// 对 obj.count 执行 CAS:如果当前值等于 0,则改为 1
boolean success = unsafe.compareAndSwapInt(obj, offset, 0, 1);

// 典型用法:CAS 自旋循环(spin loop)
int oldVal;
do {
    oldVal = obj.count;  // 读取当前值
} while (!unsafe.compareAndSwapInt(obj, offset, oldVal, oldVal + 1));
// 如果 CAS 失败(别人在我读和写之间改了值),重试
CAS 操作流程:读取 → 计算 → 比较交换 1. 读取当前值 V = 10 2. 计算新值 N = V + 1 = 11 3. CAS(V, 10, 11) 内存值仍=10? = expected(10)? YES 4a. V = 11, 成功 原子更新完成 NO 4b. CAS 失败 值已被别人改了 自旋重试:重新读取当前值,重新计算,重新 CAS CAS = 乐观锁:假定无冲突,失败才重试。无加锁/解锁开销,无上下文切换
图 1CAS 操作流程:读取当前值 → 计算新值 → 原子比较交换,失败则自旋重试
CAS 为什么是"原子"的?

x86 的 CMPXCHG 指令在执行时会锁定总线或缓存行(多核上通过 MESI 协议锁住对应缓存行),保证"读-比较-写"三步在同一总线周期内完成,其他核心在此期间无法访问同一内存地址。这就是 CAS 的硬件级原子性保障。

第 3 站

AtomicInteger 源码:volatile + CAS 自旋

AtomicInteger 是 CAS 最经典的应用。它的全部核心建立在两个要素之上:volatile 保证可见性,CAS 保证原子性

AtomicInteger.java · 核心字段与构造
public class AtomicInteger extends Number implements java.io.Serializable {
    private static final Unsafe U = Unsafe.getUnsafe();
    private static final long VALUE
        = U.objectFieldOffset(AtomicInteger.class, "value");
    private volatile int value;  // ★ volatile 保证多线程可见性

    public AtomicInteger(int initialValue) { value = initialValue; }
    public final int get() { return value; }  // volatile 读,无需加锁
}

逐行拆解 getAndIncrement()(等价于 i++)的实现:

AtomicInteger.java · getAndIncrement() 自旋 CAS
public final int getAndIncrement() {
    return U.getAndAddInt(this, VALUE, 1);
}

// Unsafe.getAndAddInt 的实现(JDK 8 中在 AtomicInteger 内部展开):
public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);  // ① 读当前值(volatile 语义)
    } while (!compareAndSwapInt(o, offset, v, v + delta));
    // ② CAS: 如果当前值仍为 v,则改为 v+1
    // ③ 失败说明别人改了,回到 ① 重新读
    return v;  // 返回旧值
}

这个 do-while 就是经典的 CAS 自旋循环(spin loop)。线程不断尝试,直到 CAS 成功为止。在竞争不激烈时,通常一两次就能成功,性能远优于加锁

AtomicInteger · 其他核心方法
// compareAndSet:直接暴露 CAS 语义
public final boolean compareAndSet(int expectedValue, int newValue) {
    return U.compareAndSetInt(this, VALUE, expectedValue, newValue);
}

// incrementAndGet:等价于 ++i,返回新值
public final int incrementAndGet() {
    return U.getAndAddInt(this, VALUE, 1) + 1;
}

// getAndSet:原子设置新值并返回旧值
public final int getAndSet(int newValue) {
    return U.getAndSetInt(this, VALUE, newValue);
}

// updateAndGet:函数式更新(JDK 8+)
public final int updateAndGet(IntUnaryOperator updateFunction) {
    int prev, next;
    do {
        prev = get();
        next = updateFunction.applyAsInt(prev);
    } while (!compareAndSet(prev, next));
    return next;
}
维度AtomicInteger(CAS)synchronized
加锁无锁(乐观)有锁(悲观)
竞争低时极快(1~2 次 CAS)有 monitor 操作开销
竞争高时自旋浪费 CPUpark 让出 CPU,更合理
阻塞不会阻塞线程可能阻塞(上下文切换)
原子变量数只能保证一个变量可保护多个变量
面试回答要点

"AtomicInteger 底层是 volatile int value 加 CAS 自旋。getAndIncrement() 用 do-while 循环,先 volatile 读当前值,再 CAS 尝试将其加 1,失败则重新读重试。低竞争时性能远超 synchronized,高竞争时自旋开销增大。"

第 4 站

ABA 问题:CAS 最大的隐患

CAS 只检查"值有没有变",但不关心值是否变过又变回来了。这就是 ABA 问题:

ABA 问题时序
// 初始状态:共享变量 V = A

Thread-1:  读取 V = A
           // 被挂起,还没执行 CAS...

Thread-2:  读取 V = A
           CAS(A → B)  成功, V = B
           CAS(B → A)  成功, V = A  // 改回去了!

Thread-1:  执行 CAS(A → C)
           // 内存值 = A == expected(A) → CAS 成功!
           // 但实际上 V 经历过 A→B→A 的变化
"ABA 到底有什么危害?值不是一样吗?" —— 在简单计数器场景下,ABA 确实无害。但在链表/栈等数据结构中,ABA 可能导致灾难性后果。

真实场景:无锁栈的 ABA 问题。假设用 CAS 实现一个栈(Treiber Stack):

ABA 导致链表断裂
// 栈:top → A → B → C
Thread-1: pop()
  读取 top = A, A.next = B
  // 准备 CAS(top, A, B) 使 top 指向 B,被挂起

Thread-2: pop() A
  CAS(top, A, B) 成功, top = B  // 弹出 A
  pop() B
  CAS(top, B, C) 成功, top = C  // 弹出 B
  push() A
  CAS(top, C, A) 成功, top = A  // 重新压入 A(A.next 仍指向 B,但 B 已弹出)

Thread-1: 恢复执行
  CAS(top, A, B) → top 当前值 = A → CAS 成功!
  top = B  // 但 B 已被 Thread-2 弹出并可能回收!
  // 栈变成 top → B(已被释放的节点),A.next 仍指向 B
  // 链表结构被破坏,可能访问已回收内存!

解决方案:加版本号。每次修改不仅比较值,还比较版本号(stamp),即使值相同,版本号不同也视为"被改过"。

AtomicStampedReference.java · 带版本号的 CAS
// AtomicStampedReference:值 + 整数版本号
AtomicStampedReference<String> ref
    = new AtomicStampedReference<>("A", 1);  // 初始值"A",版本1

int stamp = ref.getStamp();  // 记录当前版本号
String val = ref.getReference();  // 记录当前值

// Thread-2 执行 A→B→A,版本号递增
ref.compareAndSet("A", "B", stamp, stamp + 1);     // stamp: 1→2
ref.compareAndSet("B", "A", stamp + 1, stamp + 2); // stamp: 2→3

// Thread-1 尝试 CAS,版本不匹配 → 失败!ABA 被检测出来
boolean ok = ref.compareAndSet(val, "C", stamp, stamp + 1);
// stamp(1) != currentStamp(3) → CAS 失败,ABA 问题解决
AtomicMarkableReference.java · 布尔标记的 CAS
// AtomicMarkableReference:值 + boolean 标记
// 适用于只关心"有没有被改过",不关心改了几次的场景
AtomicMarkableReference<Node> ref
    = new AtomicMarkableReference<>(node, false);

boolean[] markHolder = new boolean[1];
Node val = ref.get(markHolder);  // 同时读取值和标记

// compareAndSet(expectedRef, newRef, expectedMark, newMark)
ref.compareAndSet(val, newNode, false, true);
AtomicStampedReference 的底层原理

AtomicStampedReference 内部将值和版本号封装成一个 Pair 对象(private static class Pair<T> { final T reference; final int stamp; }),通过 AtomicReference<Pair<T>> 实现 CAS。每次修改都创建新的 Pair,版本号递增。CAS 比较的是 Pair 引用,因此即使"值"变回原样,Pair 对象不同,CAS 就能检测到变化。

ABA 问题面试回答

"CAS 只比较值,如果值从 A 变成 B 再变回 A,CAS 会认为没变过,这就是 ABA 问题。典型危害场景是无锁栈/链表,可能导致节点被回收后仍被引用。解决方案是使用 AtomicStampedReference,每次修改递增版本号,即使值相同版本不同也算被改过。底层是 CAS 一个包含值和版本号的 Pair 对象。"

第 5 站

Unsafe 类:CAS 的 JVM 层实现

sun.misc.Unsafe 是 JDK 内部类,提供了直接操作内存、线程调度、CAS 等"不安全"的底层能力。所有 Atomic 类的 CAS 操作最终都通过 Unsafe 完成。

Unsafe · CAS 相关方法
public final class Unsafe {
    // 获取字段在对象中的内存偏移量
    public native long objectFieldOffset(Field f);

    // 三种基本类型的 CAS 操作(native 方法,直接映射到 CPU 指令)
    public final native boolean compareAndSwapInt(
        Object o, long offset, int expected, int update);
    public final native boolean compareAndSwapLong(
        Object o, long offset, long expected, long update);
    public final native boolean compareAndSwapObject(
        Object o, long offset, Object expected, Object update);

    // volatile 语义的读写
    public native int getIntVolatile(Object o, long offset);
    public native void putIntVolatile(Object o, long offset, int val);

    // 线程调度
    public native void park(boolean isAbsolute, long time);
    public native void unpark(Object thread);
}

Unsafe 的问题在于它是 JDK 内部 API,Oracle 从未承诺其稳定性,且命名本身就在警告开发者:"这些操作可能破坏 JVM 安全模型"。从 JDK 9 开始,Java 引入了正式的替代方案:

VarHandle · JDK 9+ 的官方替代
// java.lang.invoke.VarHandle(JDK 9 引入)
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;

class Counter {
    private int count;

    // 创建 VarHandle
    private static final VarHandle COUNT;
    static {
        try {
            COUNT = MethodHandles.lookup()
                .findVarHandle(Counter.class, "count", int.class);
        } catch (Exception e) { throw new Error(e); }
    }

    public void increment() {
        // 等价于 Unsafe.compareAndSwapInt 的自旋
        int v;
        do {
            v = (int) COUNT.getVolatile(this);
        } while (!COUNT.compareAndSet(this, v, v + 1));
    }
}
维度sun.misc.Unsafejava.lang.invoke.VarHandle
定位JDK 内部类,非公开 APIJDK 9+ 正式公开 API
稳定性随时可能变更或移除标准库,长期稳定
能力范围CAS、内存读写、线程调度、内存分配等全部聚焦于变量访问模式(plain/volatile/acquire-release)
安全性可绕过访问控制、直接操作内存类型安全,受访问控制约束
未来JEP 471 已计划弃用和移除官方推荐的替代方案
为什么不直接开放 Unsafe?

Unsafe 提供 allocateMemoryputAddress 等 C 语言级别的内存操作,绕过类型系统和 GC,极易导致内存泄漏和 JVM 崩溃。VarHandle 的设计哲学是:只暴露必要的安全子集(CAS、volatile 读写、acquire/release 语义),不暴露危险操作,将底层能力纳入标准 API 管理体系。

第 6 站

LongAdder vs AtomicLong:分段思想的又一次胜利

AtomicLong 在高并发下有一个严重问题:所有线程都对同一个 volatile 变量做 CAS。16 个线程同时递增,只有一个能 CAS 成功,其余 15 个全部自旋重试——大量 CPU 时间浪费在空转上。

"AtomicLong 高并发下性能差,怎么优化?" —— 面试官在等你说出 LongAdder,以及它背后的分段累加思想。

LongAdder(JDK 8)的核心思想借鉴自 ConcurrentHashMap:把单个热点变量分散到多个 Cell 中,每个线程更新自己的 Cell,求和时聚合

LongAdder.java · 核心结构(简化)
public class LongAdder extends Striped64 {
    // 继承自 Striped64 的核心字段:
    transient volatile Cell[] cells;   // 分段数组(惰性初始化)
    transient volatile long base;      // 无竞争时直接 CAS 更新 base
    transient volatile int cellsBusy;  // 自旋锁,保护 cells 扩容/初始化

    // Cell 内部类(伪共享优化:通过 @Contended 填充缓存行)
    static final class Cell {
        volatile long value;
        Cell(long x) { value = x; }
        final boolean cas(long cmp, long val) {
            return U.compareAndSetLong(this, VALUE, cmp, val);
        }
    }
}

add(long x) 方法展示了 LongAdder 的分段策略:

Striped64.longAccumulate() · 核心累加逻辑(简化)
public void add(long x) {
    Cell[] cs; long b, v; int m; Cell c;
    // ① cells 已初始化 → 尝试更新当前线程对应的 Cell
    if ((cs = cells) != null || !casBase(b = base, b + x)) {
        boolean uncontended = true;
        int h = getProbe();  // 线程的 hash 探测值
        if (cs == null || (m = cs.length - 1) < 0 ||
            (c = cs[h & m]) == null ||
            !(uncontended = c.cas(v = c.value, v + x)))
            // ② Cell 为空或 CAS 竞争失败 → 进入长路径
            longAccumulate(x, null, uncontended);
    }
    // ③ base CAS 成功:无竞争时的快速路径,直接返回
}

// longAccumulate:竞争时的慢路径
final void longAccumulate(long x, ...) {
    for (;;) {
        if (cells 未初始化)  初始化 cells;
        else if (对应 Cell 为空) 创建新 Cell;
        else if (Cell CAS 失败) 尝试 cells 扩容(翻倍,减少冲突);
        else 对 base 做 CAS 重试;
    }
}
LongAdder 分段累加结构 Thread-1 Thread-2 Thread-3 Thread-4 Thread-5 hash 分散 Cell[] cells(分段累加数组) Cell[0] value = 47 Cell[1] value = 123 Cell[2] value = 89 Cell[3] value = 56 base = 15 无竞争时的快速路径 sum() = 47 + 123 + 89 + 56 + 15 = 330 聚合所有 Cell 的 value + base(弱一致性快照)
图 2LongAdder 分段累加:每个线程通过 hash 映射到不同 Cell,sum() 聚合所有分段
Cell 为什么要做伪共享(False Sharing)优化?

CPU 缓存以缓存行(通常 64 字节)为单位加载。如果两个 Cell 恰好落在同一缓存行,一个线程修改 Cell[0] 会导致另一个线程的 Cell[1] 缓存行失效(MESI 协议),即使它们互不相关。LongAdder 的 Cell 类使用了 @Contended 注解(或 JDK 8 中的手动 padding),在每个 Cell 前后填充空字段,确保每个 Cell 独占一条缓存行,消除伪共享。

基准测试(16 线程并发递增 1 亿次,JDK 17,Intel i7-12700H):

实现耗时(ms)吞吐量(M ops/s)说明
AtomicLong~3200~31所有线程竞争同一变量,CAS 大量失败
LongAdder~320~312分段更新,CAS 冲突大幅减少
synchronized~4800~21悲观锁,上下文切换开销大

LongAdder 在高竞争下比 AtomicLong 快 5~10 倍,核心原因:用空间换时间——牺牲额外的 Cell[] 内存,将单点竞争分散到多个 Cell 上。

LongAdder 的代价是什么?

sum() 返回的不是精确值——遍历 Cell[] 过程中其他线程仍在更新,得到的是弱一致性快照。② 额外内存开销——Cell[] 数组 + 缓存行填充。③ 不适合需要精确原子读的场景(如 compareAndSet),因为无法原子地读取所有 Cell 的总和。

LongAdder 面试回答

"AtomicLong 在高竞争下所有线程 CAS 同一个变量,大量自旋浪费 CPU。LongAdder 借鉴 ConcurrentHashMap 的分段思想,内部维护 Cell[] 数组,每个线程通过 hash 映射到自己的 Cell 上独立 CAS 累加,sum() 时遍历所有 Cell 加 base。Cell 还通过 @Contended 消除伪共享。高竞争下吞吐量可达 AtomicLong 的 5~10 倍,代价是 sum() 不保证强一致性。"

第 7 站

总结:原子类全景与选型指南

Java 的 java.util.concurrent.atomic 包提供了完整的原子类家族,按功能分为四大类:

类别类名用途
基本类型AtomicBoolean, AtomicInteger, AtomicLong原子更新布尔值/整数/长整数
数组类型AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray原子更新数组中的元素
引用类型AtomicReference, AtomicStampedReference, AtomicMarkableReference原子更新对象引用(后两者解决 ABA)
字段更新器AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater原子更新已有对象的 volatile 字段,避免为每个对象创建 Atomic 包装

JDK 8 新增的高性能累加器:

类名适用场景核心优势
LongAdder / DoubleAdder高并发计数/求和分段 CAS,吞吐量 5~10x
LongAccumulator / DoubleAccumulator高并发聚合(max/min/sum 等)支持自定义累加函数,通用性更强
选型决策树
低竞争计数器 → AtomicInteger / AtomicLong
高竞争计数器 → LongAdder(只需 add + sum)
高竞争 + 自定义聚合 → LongAccumulator
需要 CAS 引用 + 防 ABA → AtomicStampedReference
需要原子更新对象字段 → AtomicIntegerFieldUpdater(零额外对象开销)
需要精确原子读 → AtomicLong(LongAdder 的 sum 是弱一致的)

面试回答模板

Q:CAS 是什么?有什么问题?怎么解决?

"CAS(Compare And Swap)是 CPU 原子指令 CMPXCHG 的 Java 封装,通过比较内存值与预期值是否一致来决定是否更新。优点是无锁、无阻塞、低竞争时性能极好。三个问题:① ABA 问题——值变过又变回来,CAS 无法感知,用 AtomicStampedReference 加版本号解决;② 自旋开销——高竞争时大量线程 CAS 失败空转,浪费 CPU,用 LongAdder 分段解决;③ 只能保证单个变量原子性——多个变量需要用锁或 AtomicReference 包装成单个对象。"

Q:LongAdder 为什么比 AtomicLong 快?

"AtomicLong 所有线程 CAS 同一个 volatile long,高竞争下 16 个线程只有 1 个成功,其余自旋。LongAdder 用 Cell[] 数组分散竞争,每个线程通过 hash 映射到自己的 Cell 独立 CAS 累加,sum() 时聚合所有 Cell + base。Cell 还通过 @Contended 消除伪共享。高竞争下快 5~10 倍,但 sum() 是弱一致的,不适合需要精确原子读的场景。"

Q:Unsafe 和 VarHandle 的区别?

"Unsafe 是 JDK 内部类,提供 CAS、内存操作等全部底层能力,但非公开 API,随时可能变更。VarHandle 是 JDK 9 引入的官方替代,只暴露安全的变量访问模式(CAS、volatile 读写、acquire-release),类型安全且长期稳定。JEP 471 已计划逐步移除 Unsafe。"