Lesson 21 · 并发编程
CAS 与原子操作:AtomicInteger 到 LongAdder
面试官:不加锁怎么保证原子性?
在并发编程中,synchronized 和 Lock 是悲观策略——假定冲突一定会发生,所以先加锁再操作。而 CAS(Compare And Swap)是乐观策略——假定冲突很少发生,更新时检查是否被别人改过,没改过就原子更新,改过就重试。
基于 CAS 实现的原子类(AtomicInteger、AtomicLong、AtomicReference 等)是 Java 并发工具箱中最高效的同步手段。它们无需加锁、无需线程上下文切换,在低竞争场景下性能远超锁方案。但 CAS 并非银弹——ABA 问题、自旋开销、只能保证单个变量原子性是它的三大局限。
本文从 CPU 指令级原理出发,逐层拆解 CAS → AtomicInteger → ABA 问题 → Unsafe → LongAdder,构建完整的无锁并发知识体系。
CPU 指令 CMPXCHG → CAS 算法 → AtomicInteger 源码 → ABA 问题与解决方案 → Unsafe 与 VarHandle → LongAdder 分段优化
CAS 原理:从 CPU 指令到 Java API
CAS 的全称是 Compare And Swap(比较并交换),它是一条 CPU 原子指令(x86 上是 CMPXCHG),能在单条指令内完成"读-比较-写"三步操作,不会被其他线程打断。
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 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 失败(别人在我读和写之间改了值),重试
x86 的 CMPXCHG 指令在执行时会锁定总线或缓存行(多核上通过 MESI 协议锁住对应缓存行),保证"读-比较-写"三步在同一总线周期内完成,其他核心在此期间无法访问同一内存地址。这就是 CAS 的硬件级原子性保障。
AtomicInteger 源码:volatile + CAS 自旋
AtomicInteger 是 CAS 最经典的应用。它的全部核心建立在两个要素之上:volatile 保证可见性,CAS 保证原子性。
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++)的实现:
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 成功为止。在竞争不激烈时,通常一两次就能成功,性能远优于加锁。
// 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 操作开销 |
| 竞争高时 | 自旋浪费 CPU | park 让出 CPU,更合理 |
| 阻塞 | 不会阻塞线程 | 可能阻塞(上下文切换) |
| 原子变量数 | 只能保证一个变量 | 可保护多个变量 |
"AtomicInteger 底层是 volatile int value 加 CAS 自旋。getAndIncrement() 用 do-while 循环,先 volatile 读当前值,再 CAS 尝试将其加 1,失败则重新读重试。低竞争时性能远超 synchronized,高竞争时自旋开销增大。"
ABA 问题:CAS 最大的隐患
CAS 只检查"值有没有变",但不关心值是否变过又变回来了。这就是 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 问题。假设用 CAS 实现一个栈(Treiber Stack):
// 栈: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:值 + 整数版本号
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:值 + 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 内部将值和版本号封装成一个 Pair 对象(private static class Pair<T> { final T reference; final int stamp; }),通过 AtomicReference<Pair<T>> 实现 CAS。每次修改都创建新的 Pair,版本号递增。CAS 比较的是 Pair 引用,因此即使"值"变回原样,Pair 对象不同,CAS 就能检测到变化。
"CAS 只比较值,如果值从 A 变成 B 再变回 A,CAS 会认为没变过,这就是 ABA 问题。典型危害场景是无锁栈/链表,可能导致节点被回收后仍被引用。解决方案是使用 AtomicStampedReference,每次修改递增版本号,即使值相同版本不同也算被改过。底层是 CAS 一个包含值和版本号的 Pair 对象。"
Unsafe 类:CAS 的 JVM 层实现
sun.misc.Unsafe 是 JDK 内部类,提供了直接操作内存、线程调度、CAS 等"不安全"的底层能力。所有 Atomic 类的 CAS 操作最终都通过 Unsafe 完成。
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 引入了正式的替代方案:
// 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.Unsafe | java.lang.invoke.VarHandle |
|---|---|---|
| 定位 | JDK 内部类,非公开 API | JDK 9+ 正式公开 API |
| 稳定性 | 随时可能变更或移除 | 标准库,长期稳定 |
| 能力范围 | CAS、内存读写、线程调度、内存分配等全部 | 聚焦于变量访问模式(plain/volatile/acquire-release) |
| 安全性 | 可绕过访问控制、直接操作内存 | 类型安全,受访问控制约束 |
| 未来 | JEP 471 已计划弃用和移除 | 官方推荐的替代方案 |
Unsafe 提供 allocateMemory、putAddress 等 C 语言级别的内存操作,绕过类型系统和 GC,极易导致内存泄漏和 JVM 崩溃。VarHandle 的设计哲学是:只暴露必要的安全子集(CAS、volatile 读写、acquire/release 语义),不暴露危险操作,将底层能力纳入标准 API 管理体系。
LongAdder vs AtomicLong:分段思想的又一次胜利
AtomicLong 在高并发下有一个严重问题:所有线程都对同一个 volatile 变量做 CAS。16 个线程同时递增,只有一个能 CAS 成功,其余 15 个全部自旋重试——大量 CPU 时间浪费在空转上。
LongAdder(JDK 8)的核心思想借鉴自 ConcurrentHashMap:把单个热点变量分散到多个 Cell 中,每个线程更新自己的 Cell,求和时聚合。
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 的分段策略:
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 重试;
}
}
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 上。
① sum() 返回的不是精确值——遍历 Cell[] 过程中其他线程仍在更新,得到的是弱一致性快照。② 额外内存开销——Cell[] 数组 + 缓存行填充。③ 不适合需要精确原子读的场景(如 compareAndSet),因为无法原子地读取所有 Cell 的总和。
"AtomicLong 在高竞争下所有线程 CAS 同一个变量,大量自旋浪费 CPU。LongAdder 借鉴 ConcurrentHashMap 的分段思想,内部维护 Cell[] 数组,每个线程通过 hash 映射到自己的 Cell 上独立 CAS 累加,sum() 时遍历所有 Cell 加 base。Cell 还通过 @Contended 消除伪共享。高竞争下吞吐量可达 AtomicLong 的 5~10 倍,代价是 sum() 不保证强一致性。"
总结:原子类全景与选型指南
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。"