CAS和Atomic深入分析
# 37.CAS和Atomic深入分析
# 目录介绍
- 1. 案例引入
- 2. CAS 的物理基础
- 3. Atomic 全家桶源码剖析
- 4. ABA 问题与解决
- 5. LongAdder & Striped64 分段累加革命
- 6. Unsafe 时代的"危险但强大"
- 7. VarHandle 取代 Unsafe
- 8. 性能实证
- 9. 综合回扣与设计哲学
# 1. 案例引入
# 1.1 AtomicLong 高并发的吞吐天花板
某金融风控系统的全局请求计数器,用 AtomicLong 实现:"不就是个原子自增吗,CAS 妥妥的零锁":
public class RequestCounter {
private final AtomicLong count = new AtomicLong();
public void inc() { count.incrementAndGet(); }
public long get() { return count.get(); }
}
2
3
4
5
单机 4 核压测无压力——100 万 QPS。上线后扩容到 32 核,期望线性提升到 800 万——实测只有 280 万 QPS,CPU 却被打满。perf top 一看:
32.5% java __cmpxchg16b ← CAS 重试
18.2% java AtomicLong.incrementAndGet
...
2
3
32 核机器上 32 个线程都在同一个内存地址上 CAS——绝大多数时间在 CAS 失败 + 重试。换成 LongAdder:
private final LongAdder count = new LongAdder();
public void inc() { count.increment(); }
public long get() { return count.sum(); }
2
3
实测 2400 万 QPS,CPU 降到 60%——吞吐提升 8 倍。同样是无锁,差距为什么这么大?这是 §5 要回答的核心。
# 1.2 ABA 引发的转账幽灵 bug
某支付系统用 AtomicReference<Account> 做余额无锁更新,"反正 CAS 能保证原子性":
AtomicReference<Account> ref = new AtomicReference<>(acc);
Account old = ref.get();
Account neu = old.withBalance(old.balance - 100);
boolean ok = ref.compareAndSet(old, neu); // ← CAS 失败就重试
2
3
4
某天对账:1000 笔扣款,账上只少了 800——20 万元凭空消失。
排查发现:高并发下出现这种交错:
T1: 读 old = Acc(100元) → 准备扣 100
T2: 读 acc = Acc(100元) → 扣 100 → ref = Acc(0元)
T2: 又来 100 元入账 → ref = Acc(100元) ← 同一个引用对象池复用
T1: CAS(old=Acc(100), neu=Acc(0)) ← old.balance=100 == 当前 100 → 成功!
T2 的入账被覆盖丢失!
2
3
4
5
CAS 比的是"地址相等",但同一地址的对象状态可能已经"绕了一圈又回到了起点"——这就是 ABA 问题。compareAndSet 看似成功,业务实际丢了 200 元。怎么破? —— §4 给出标准方案。
# 1.3 我们要回答什么
38 篇是卷五第 7 篇,沉到 36/37 篇 Lock 之下、CPU 指令之上的"无锁原语"层:
37.Lock 三剑客 (用户态阻塞) → 38.CAS/Atomic (本篇/无锁原语) → 39.五大同步器
│
↓
Lock/AQS 内部用的全是 CAS(36 篇 §3 见过)
2
3
4
带 6 个追问展开:
追问 ①:CAS 凭什么是原子的?JVM 怎么把它落到 CPU 指令? → §2
追问 ②:Atomic 全家桶怎么覆盖整数/对象/数组/字段四种场景? → §3
追问 ③:ABA 问题为什么必须重视?正确防护代价多少? → §4
追问 ④:LongAdder 凭什么比 AtomicLong 在高竞争下快 10 倍? → §5
追问 ⑤:Unsafe 那么强为什么要被 VarHandle 取代? → §6 + §7
追问 ⑥:什么时候用 AtomicLong,什么时候必须换 LongAdder? → §8
2
3
4
5
6
# 2. CAS 的物理基础
# 2.1 CAS 三要素与伪代码语义
CAS = Compare And Swap,三个参数:
CAS(addr, expected, newValue):
if (*addr == expected):
*addr = newValue
return true
else:
return false
2
3
4
5
6
关键:上面这"读 → 比较 → 写"三步必须原子完成——不能被任何中断、任何其他 CPU 的写打断。这不是普通的 if-else,是硬件层面的原子性。
如果用普通指令实现:
LOAD R1, [addr] ← 读
CMP R1, expected ← 比
← ⚠️ 此时其他 CPU 改了 *addr 怎么办?
STORE [addr], newValue ← 写
2
3
4
中间任意一步都可能被打断——所以 CAS 必须是单条原子指令,靠 CPU 硬件保证。
# 2.2 LOCK CMPXCHG:x86 的原子保证
x86 平台上,CAS 落到一条指令——CMPXCHG,前面加 LOCK 前缀强制原子:
lock cmpxchgq %rdx, (%rdi) ; if (*rdi == %rax) *rdi = %rdx; else %rax = *rdi
LOCK 前缀做了什么?早期是锁总线——这条指令期间整条内存总线被独占,其他 CPU 的内存访问全卡住。现代 CPU(Pentium Pro 以后)改为缓存锁定——只锁住该内存地址所在的缓存行(cache line),其他 CPU 访问无关地址不受影响,效率更高。
OpenJDK HotSpot 中 Atomic::cmpxchg 在 x86 的实现:
// hotspot/src/os_cpu/linux_x86/atomic_linux_x86.hpp
template<>
inline jint Atomic::cmpxchg<jint>(jint exchange_value, volatile jint* dest, jint compare_value) {
__asm__ volatile ("lock cmpxchgl %1, (%3)"
: "=a"(exchange_value)
: "r"(exchange_value), "a"(compare_value), "r"(dest)
: "cc", "memory");
return exchange_value;
}
2
3
4
5
6
7
8
9
lock cmpxchgl 一条指令 = Java 层 compareAndSet 的全部物理实现。任意 Java 的 CAS 调用,最终都收敛到这条 CPU 指令。
# 2.3 缓存一致性 MESI 与 CAS 的代价
CAS 不是"免费"的。理解它的代价必须懂 MESI 缓存一致性协议:
M (Modified) :本核独占且已修改,主存数据是脏的
E (Exclusive) :本核独占未修改
S (Shared) :多核共享只读
I (Invalid) :无效(被其他核修改了)
2
3
4
CAS 一条指令的真实开销:
1. 把 cache line 从 S 升级到 M(需要 RFO - Read For Ownership 请求)
2. 广播 Invalidate 给其他核(其他核的副本失效)
3. 等所有核 ACK 回来
4. 执行 cmpxchg
5. cache line 状态变为 M
2
3
4
5
热点 CAS = 全核间不停广播 Invalidate——这就是 §1.1 中 32 核 AtomicLong 翻车的根因:32 核都在抢同一个 cache line 的所有权,每次 CAS 都触发全局缓存一致性风暴。
# 2.4 ARM 的 LL/SC 原语对比
ARM/POWER 等 RISC 架构没有 CAS 单指令,用 LL/SC(Load-Linked / Store-Conditional) 模拟:
loop:
LDREX R1, [addr] ; LL:读并打标记
CMP R1, expected
BNE fail
STREX R2, newValue, [addr] ; SC:仅当标记仍在才写成功,R2=0
CBNZ R2, loop ; SC 失败则重试
2
3
4
5
6
特点:LL/SC 通过"读时打标记 + 写时检查标记"实现原子性,标记被任何写操作清除——天然抵抗 ABA(在硬件层面)。但 Java 层暴露的还是 CAS 语义——JVM 在 ARM 上用 LL/SC 模拟 CAS,对 Java 程序员透明。
这是为什么 ARM 上同样的 Java 代码在写多场景下表现可能略好——LL/SC 不需要 RFO 独占 cache line。
# 2.5 自旋的开销与上限
CAS 失败后通常自旋重试:
public final int incrementAndGet() {
int prev, next;
do {
prev = get();
next = prev + 1;
} while (!compareAndSet(prev, next)); // ← 失败就重试
return next;
}
2
3
4
5
6
7
8
自旋的代价:
- CPU 空转——重试期间消耗 CPU 时间片
- Cache line 抢夺——每次重试都重新拉缓存行
- 能量浪费——移动设备发热
自旋什么时候划算:临界区极短(几条指令)+ 竞争不激烈(< 4 线程同时)。否则就该用锁——这是 36/37 篇 Lock 存在的根本原因。LongAdder 的设计目标就是让 CAS 自旋次数下降一个数量级(§5)。
# 3. Atomic 全家桶源码剖析
# 3.1 四大类家族总览
java.util.concurrent.atomic 包按用途分四大类:
| 类别 | 代表类 | 用途 |
|---|---|---|
| 基本类型 | AtomicInteger / AtomicLong / AtomicBoolean | 单个原子变量 |
| 引用类型 | AtomicReference / AtomicStampedReference / AtomicMarkableReference | 对象引用原子 |
| 数组类型 | AtomicIntegerArray / AtomicLongArray / AtomicReferenceArray | 数组元素原子 |
| 字段更新器 | AtomicIntegerFieldUpdater / AtomicLongFieldUpdater / AtomicReferenceFieldUpdater | 已有类的字段改造为原子 |
| 累加器(§5) | LongAdder / LongAccumulator / DoubleAdder / DoubleAccumulator | 高竞争下的优化版 |
# 3.2 AtomicInteger 核心源码
public class AtomicInteger extends Number implements java.io.Serializable {
private static final VarHandle VALUE; // ← JDK 9 起换 VarHandle
static {
try {
MethodHandles.Lookup l = MethodHandles.lookup();
VALUE = l.findVarHandle(AtomicInteger.class, "value", int.class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}
private volatile int value; // ★ volatile 保证可见性
public final int get() { return value; } // 直接读 volatile
public final void set(int newValue) { value = newValue; } // 直接写 volatile
public final boolean compareAndSet(int expectedValue, int newValue) {
return VALUE.compareAndSet(this, expectedValue, newValue); // ★ VarHandle.CAS
}
public final int incrementAndGet() {
return (int) VALUE.getAndAdd(this, 1) + 1; // ★ JDK 8 还是手写 CAS 自旋
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
JDK 7 → JDK 8 → JDK 9 的演进:
- JDK 7-:自己写 CAS 自旋循环
- JDK 8:
Unsafe.getAndAddInt内部仍是自旋,但部分平台编译为 LOCK XADD 单指令(intrinsic 优化) - JDK 9+:底层换为
VarHandle,逻辑不变但符合模块化(§7)
// JDK 8 Unsafe.getAndAddInt 内部
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta)); // ← 失败重试
return v;
}
2
3
4
5
6
7
8
注意:HotSpot 对 getAndAddInt 有 intrinsic 替换——若 CPU 支持 LOCK XADD(x86 自带),整个循环会被替换为一条 lock xaddl 指令,无需软件自旋。这是为什么 incrementAndGet 在 x86 上比看起来快。
# 3.3 AtomicReference 与泛型
public class AtomicReference<V> implements java.io.Serializable {
private static final VarHandle VALUE;
private volatile V value;
public final boolean compareAndSet(V expectedValue, V newValue) {
return VALUE.compareAndSet(this, expectedValue, newValue);
}
}
2
3
4
5
6
7
8
注意:compareAndSet(expected, new) 比的是引用相等(==),不是 equals!这就是 §1.2 ABA 问题的根源——同一引用值的对象内部状态可能已变化。
典型用法:无锁链表:
class LockFreeStack<T> {
private final AtomicReference<Node<T>> top = new AtomicReference<>();
public void push(T item) {
Node<T> oldTop, newTop;
do {
oldTop = top.get();
newTop = new Node<>(item, oldTop);
} while (!top.compareAndSet(oldTop, newTop)); // ← 经典 CAS 重试
}
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.item;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
警告:上面的 pop 在节点对象池场景下会发生 ABA 问题——节点出栈后又重用入栈,CAS 仍成功但中间状态丢失。Treiber Stack 必须改用 AtomicStampedReference(§4.2)。
# 3.4 AtomicIntegerArray 数组原子
数组的特殊性:单元素原子操作,不是整个数组:
public class AtomicIntegerArray implements java.io.Serializable {
private static final VarHandle AA = MethodHandles.arrayElementVarHandle(int[].class);
private final int[] array;
public final boolean compareAndSet(int i, int expectedValue, int newValue) {
return AA.compareAndSet(array, i, expectedValue, newValue);
}
}
2
3
4
5
6
7
8
性能注意:相邻数组元素可能落在同一 cache line(一个 cache line 通常 64 字节,可装 16 个 int)——多线程操作相邻索引会引发伪共享,性能比独立 AtomicInteger 还差。这正是 LongAdder 用 @Contended 防护的原因(§5.6)。
# 3.5 AtomicXxxFieldUpdater 反射大法
场景:已有的类用了 volatile int counter,现在想原子更新——不想改成 AtomicInteger(侵入性大)。
class Connection {
volatile int state; // ← 已有字段,不能动
private static final AtomicIntegerFieldUpdater<Connection> STATE_UPDATER =
AtomicIntegerFieldUpdater.newUpdater(Connection.class, "state");
public boolean tryClose() {
return STATE_UPDATER.compareAndSet(this, OPEN, CLOSED);
}
}
2
3
4
5
6
7
8
9
10
好处:
- 零内存开销:每实例不需要额外的 AtomicInteger 包装对象(节省一个对象头 + 一个引用)
- 遗留代码改造友好:不需要重构整个类
限制:
- 字段必须是
volatile - 字段不能是
private(JDK 9 之前需要包私有以上可见性) - 反射开销(创建 Updater 时一次性,后续 CAS 无开销)
典型用例:Netty 的 ChannelPromise、AQS 的 state 字段早期都用过 FieldUpdater;JDK 9+ 后内部改为 VarHandle,但 FieldUpdater 仍保留兼容老代码。
# 4. ABA 问题与解决
# 4.1 ABA 的本质:值相等 ≠ 状态未变
ABA 经典三步剧本:
T1: 读取 X,期望值 = A
T2: X: A → B → A (改了又改回来)
T1: CAS(X, A, C) → 成功(误以为 X 没动过)
2
3
为什么是问题:CAS 的语义是"基于'我读到的状态'做更新"——但状态可能已经历完整周期。在以下场景里 ABA 会引发真实 bug:
✗ 对象池复用 :节点出池入池,地址重用
✗ 链表无锁 :Treiber Stack 弹出 + 推入相同节点
✗ 余额扣款 :§1.2 中入账被覆盖
2
3
特殊情况下 ABA 不是问题:纯计数器(AtomicLong 自增)——只关心最终值的正确性,中间过程不重要。所以不是所有 CAS 都需要防 ABA,要看业务语义。
# 4.2 AtomicStampedReference 版本号方案
用一个版本戳伴随引用——每次 CAS 时同时检查值和戳,戳每次更新单调递增:
public class AtomicStampedReference<V> {
private static class Pair<T> {
final T reference;
final int stamp;
}
private volatile Pair<V> pair;
public boolean compareAndSet(V expectedReference, V newReference,
int expectedStamp, int newStamp) {
Pair<V> current = pair;
return expectedReference == current.reference &&
expectedStamp == current.stamp && // ★ 同时检查戳
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
用法:
AtomicStampedReference<Account> ref = new AtomicStampedReference<>(acc, 0);
int[] stampHolder = new int[1];
Account old = ref.get(stampHolder);
int oldStamp = stampHolder[0];
Account neu = old.withBalance(old.balance - 100);
boolean ok = ref.compareAndSet(old, neu, oldStamp, oldStamp + 1); // ★ 戳 +1
2
3
4
5
6
7
8
戳的作用:哪怕引用又绕回 A,戳早已不是原来的 oldStamp——CAS 失败重试,避免幽灵 bug。这就是 §1.2 的标准修复。
类比:MVCC 的事务版本号、Git 的 commit hash、HTTP 的 ETag——都是同一个思路:用单调递增的版本号区分"看似相同的状态"。
# 4.3 AtomicMarkableReference 单标志方案
如果只关心"被改过没改过",不关心改了多少次——用一位 boolean 比一个 int 更省:
public class AtomicMarkableReference<V> {
private static class Pair<T> {
final T reference;
final boolean mark;
}
public boolean compareAndSet(V expectedReference, V newReference,
boolean expectedMark, boolean newMark) {
// 同时比较 reference + mark
}
}
2
3
4
5
6
7
8
9
10
11
典型场景:无锁链表的"逻辑删除"——节点上挂一个 mark=true 表示已删除,物理移除推迟到下次 CAS:
// CompletableFuture 的内部就用了类似的 mark 机制
node.casNext(oldNext, newNext, oldMark, true) // 标记删除
2
# 4.4 工程视角:哪些场景必须防 ABA
✅ 必须防 ABA:
- 对象池 / 节点池 → 用 AtomicStampedReference
- 无锁数据结构(Stack/Queue)→ 同上
- 状态机有"绕回"语义(如 A → B → A 是合法转换)
❌ 不需要防 ABA:
- 纯计数器(AtomicLong 自增)
- 单调递增的时间戳/序号
- 状态机不会绕回(如 OPEN → CLOSED 不可逆)
2
3
4
5
6
7
8
9
经验法则:业务上"是否经历过 B 状态"会影响后续行为 → 必须防;反之可以省。
# 5. LongAdder & Striped64 分段累加革命
# 5.1 AtomicLong 高并发瓶颈的根因
回到 §1.1:32 核机器 32 线程对同一个 AtomicLong 自增:
所有线程都在抢同一个 cache line:
┌────────────────────────┐
│ AtomicLong.value (8B) │ ← 32 核都在 CAS 同一地址
└────────────────────────┘
↑
┌─────┴────────────────────────┐
│ Core0 Core1 Core2 ... Core31 │
└──────────────────────────────┘
每个 CAS 都触发 RFO + Invalidate 广播 → 缓存一致性风暴
2
3
4
5
6
7
8
9
10
AtomicLong 高竞争下 CAS 失败率极高——理论上无锁,实际上"软件锁"(自旋等价于忙等待)。
# 5.2 LongAdder 核心思想:化整为零
LongAdder 的核心思想可以一句话概括:
不要让所有线程都改同一个变量——把热点拆成 N 个,每个线程只改自己那一份;要总和时再加起来。
AtomicLong: LongAdder:
[ value ] ←──── 32 核 base ← 低竞争时用 base
[Cell0][Cell1]...[Cell15]
↑ ↑ ↑
T0 T1 ... T31 ← 高竞争时用 Cell 数组
sum() = base + sum(Cells)
2
3
4
5
6
7
8
代价:读取总和(sum)不再是 O(1)——需要遍历所有 Cell 累加,且不是强一致(遍历期间可能有 Cell 被写入)。收益:写吞吐线性可扩展——核数翻倍,吞吐近乎翻倍。
这是典型的"用读时复杂换写时吞吐"trade-off——和 ConcurrentHashMap 分段、Java 7 ForkJoinPool 队列分段一脉相承。
# 5.3 Striped64 数据结构
LongAdder 继承自 Striped64(DoubleAdder 也继承它),核心字段:
abstract class Striped64 extends Number {
transient volatile Cell[] cells; // ★ Cell 数组(懒初始化)
transient volatile long base; // ★ 基础值(无竞争时直接累加这里)
transient volatile int cellsBusy; // ★ 自旋锁(保护 cells 扩容/初始化)
@jdk.internal.vm.annotation.Contended
static final class Cell { // ★ 单元格 + 伪共享防护
volatile long value;
Cell(long x) { value = x; }
final boolean cas(long cmp, long val) { return VALUE.compareAndSet(this, cmp, val); }
}
}
2
3
4
5
6
7
8
9
10
11
12
关键设计:
base—— 无竞争快路径cells[]—— 高竞争时分散到多个槽位cellsBusy—— 1 字节自旋锁,仅保护数组创建/扩容Cell用@Contended注解避免伪共享(§5.6)
# 5.4 add 方法热点路径
public void add(long x) {
Cell[] cs; long b, v; int m; Cell c;
if ((cs = cells) != null || !casBase(b = base, b + x)) {
// ★ 走到这里说明:① cells 已建(高竞争已发生)或 ② CAS base 失败一次
boolean uncontended = true;
if (cs == null || (m = cs.length - 1) < 0 ||
(c = cs[getProbe() & m]) == null || // ★ 用线程探针选 Cell
!(uncontended = c.cas(v = c.value, v + x))) { // ★ CAS Cell.value
longAccumulate(x, null, uncontended); // ↑ 失败 → 慢路径(扩容/重定位)
}
}
}
2
3
4
5
6
7
8
9
10
11
12
双重快慢路径:
- 极速路径:
casBase(base, base + x)一把成功 → 完事(无竞争场景) - 快路径:用
getProbe() & (cells.length - 1)选 Cell,CAS 成功 → 完事 - 慢路径:
longAccumulate—— Cell 扩容、线程探针重设、自旋抢 cellsBusy
线程探针 getProbe():每个线程从 Thread.threadLocalRandomProbe 取一个伪随机值——保证不同线程大概率分布到不同 Cell。
# 5.5 Cell 动态扩容机制
longAccumulate 的核心扩容逻辑(简化):
final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {
int h;
if ((h = getProbe()) == 0) { ThreadLocalRandom.current(); h = getProbe(); } // ① 初始化探针
boolean collide = false; // ② 是否需要扩容
for (;;) {
Cell[] cs; Cell c; int n; long v;
if ((cs = cells) != null && (n = cs.length) > 0) {
if ((c = cs[(n - 1) & h]) == null) { // ③ 该位置空 → 创建 Cell
if (cellsBusy == 0 && casCellsBusy()) { // 抢自旋锁
try { /* 创建并放入 */ } finally { cellsBusy = 0; }
}
} else if (c.cas(v = c.value, v + x)) break; // ④ CAS 成功 → 完事
else if (n >= NCPU || cells != cs) collide = false; // ⑤ 已到 CPU 数上限,不扩容
else if (!collide) collide = true;
else if (cellsBusy == 0 && casCellsBusy()) { // ⑥ 扩容:长度翻倍
try { Cell[] rs = new Cell[n << 1]; ... } finally { cellsBusy = 0; }
collide = false;
}
h = advanceProbe(h); // ⑦ 重设探针,下轮换槽
} else if (cellsBusy == 0 && cells == cs && casCellsBusy()) {
// ⑧ 第一次初始化 cells 数组(默认 size = 2)
try { cells = new Cell[]{ new Cell(x) }; } finally { cellsBusy = 0; }
break;
} else if (casBase(v = base, v + x)) break; // ⑨ 兜底:直接累加 base
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
关键设计点:
- 数组长度上限 = NCPU(CPU 核心数向上 2 的幂)—— 超过就没意义了
- 2 倍扩容 —— 类似 HashMap,& 运算定位槽位
- collide 标志 —— 连续两次冲突才扩容,避免抖动
- probe 重设 —— 同线程多次重试时换槽,分散竞争
# 5.6 @Contended 伪共享防护
伪共享(False Sharing):两个独立变量恰好落在同一 cache line,多核分别更新时仍引发 cache line 抢夺——明明是独立变量,性能却像在抢同一个变量。
没有 @Contended:
cache line (64B) ┌────────────┬────────────┬────────────┐
│ Cell0.value│ Cell1.value│ Cell2.value│
└────────────┴────────────┴────────────┘
↑ ↑ ↑
Core0 Core1 Core2
⚠️ 三个核相互 invalidate,伪共享触发
2
3
4
5
6
7
@Contended 解决方案:在 Cell 前后填充字节,确保每个 Cell 独占一条 cache line:
@jdk.internal.vm.annotation.Contended
static final class Cell {
volatile long value;
}
// 实际内存布局(JVM 处理):
// [前 128B padding][value 8B][后 128B padding]
// → 每个 Cell 占用 ~144B,独占两条 cache line
2
3
4
5
6
7
JDK 启动参数:@Contended 需要 -XX:-RestrictContended 才能在用户代码生效(默认仅 JDK 内部类启用)。自定义类要用伪共享防护,必须开启此参数或自行手写 padding。
没有伪共享防护的话:LongAdder 的吞吐会下降到 AtomicLong 的水平——这才是它真正性能爆杀的关键。
# 5.7 sum 方法的弱一致性
public long sum() {
Cell[] cs = cells;
long sum = base;
if (cs != null) {
for (Cell c : cs) {
if (c != null) sum += c.value; // ★ 遍历期间 Cell 仍可能被写
}
}
return sum;
}
2
3
4
5
6
7
8
9
10
警告:sum() 不是强一致快照——遍历过程中其他线程可能仍在修改 Cell。如果业务需要严格一致的快照(如对账),必须配合外部锁;但 95% 的统计场景(如 QPS、计数器、累计值)最终一致就够了。
sumThenReset():边读边重置,用于"周期性归零统计"——但同样不是原子的。
# 5.8 LongAccumulator/DoubleAdder 兄弟
| 类 | 用途 | 操作 |
|---|---|---|
| LongAdder | 求和 | +x |
| LongAccumulator | 任意二元运算 | accum.apply(prev, x) |
| DoubleAdder | double 求和 | +x(精度受限) |
| DoubleAccumulator | double 任意运算 | 同上 |
LongAccumulator 例子——并发求最大值:
LongAccumulator max = new LongAccumulator(Long::max, Long.MIN_VALUE);
max.accumulate(42);
max.accumulate(100);
max.accumulate(7);
long result = max.get(); // → 100
2
3
4
5
底层完全一样的 Striped64,只是把 + 换成自定义函数。注意:函数必须满足结合律 + 无副作用——因为 sum 顺序不确定。
# 6. Unsafe 时代的"危险但强大"
# 6.1 Unsafe 五大能力域
sun.misc.Unsafe(JDK 9+ 改名 jdk.internal.misc.Unsafe)是 JDK 内部"黑魔法"工具箱:
| 能力域 | 代表 API | 用途 |
|---|---|---|
| CAS | compareAndSwapInt/Long/Object | 原子原语 |
| 直接内存 | allocateMemory / freeMemory | 堆外内存(DirectByteBuffer) |
| 线程调度 | park / unpark | LockSupport 底座 |
| 对象操作 | objectFieldOffset / putXxx | 直接读写字段 |
| 类操作 | defineClass / allocateInstance | 绕过构造方法 |
Unsafe 是 Java 并发体系的"最底层水源"——AQS 的 state CAS、Lock 的 park、Atomic 的 compareAndSwap、DirectByteBuffer 的内存分配、CompletableFuture 的状态机——全都建在 Unsafe 上。
# 6.2 compareAndSwapXxx:CAS 的本尊
// JDK 8 Unsafe 核心 API
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);
public final native boolean compareAndSwapInt (Object o, long offset, int expected, int x);
public final native boolean compareAndSwapLong (Object o, long offset, long expected, long x);
2
3
4
native 实现就是 §2.2 看到的 lock cmpxchgl。Java 层任意 CAS 调用最终都收敛到这里。
HotSpot intrinsic 优化:JIT 识别 Unsafe.compareAndSwapInt,直接替换为 CPU 指令而非函数调用——零调用开销。
# 6.3 park/unpark:Lock/AQS 的物理基础
public native void park(boolean isAbsolute, long time); // 当前线程阻塞
public native void unpark(Object thread); // 唤醒指定线程
2
与 wait/notify 的根本区别:
| 维度 | wait/notify | park/unpark |
|---|---|---|
| 必须持锁? | 必须持 monitor | 不需要 |
| 唤醒粒度 | notify 不指定 / notifyAll 全唤醒 | unpark 精确指定线程 |
| 顺序敏感? | notify 在 wait 前 → 信号丢失 | unpark 在 park 前 → 不丢失(凭证机制) |
凭证(permit)机制:每个线程有一个二值信号量(0 或 1)。unpark 把 permit 设为 1(已经是 1 也不累加);park 检查 permit——是 1 就消耗后立即返回,是 0 就阻塞。这就是为什么 LockSupport 不会丢失信号。36 篇 AQS 把整套同步框架建在 park/unpark 上,根因就在此。
# 6.4 allocateMemory 与堆外内存
long addr = unsafe.allocateMemory(1024); // 分配 1KB 堆外内存
unsafe.putLong(addr, 42L);
long v = unsafe.getLong(addr);
unsafe.freeMemory(addr);
2
3
4
用途:
DirectByteBuffer—— NIO 零拷贝(卷六 42-44 篇会展开)- Netty 的 PooledByteBufAllocator —— 堆外内存池
- Apache Arrow / RocksDB 的 off-heap 数据结构
风险:手动分配,不释放就泄漏——而且不被 GC 管理,泄漏不报 OOM 报"native OOM"。这是 16 篇 OOM 八大现场之一。
# 6.5 Unsafe 的获取黑魔法
// ❌ 直接 Unsafe.getUnsafe() —— 系统类才有权限
Unsafe unsafe = Unsafe.getUnsafe(); // SecurityException
// ✅ 反射拿("破解"方式)
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
2
3
4
5
6
7
为什么 JDK 要禁止用户代码用 Unsafe:能力太强 —— 可以绕过 final、绕过类型检查、写任意内存——一个错误就崩 JVM。所以才要 VarHandle 取代它(§7)。
# 7. VarHandle 取代 Unsafe
# 7.1 JDK 9 模块化对 Unsafe 的封锁
JDK 9 模块化(JEP 261)后,sun.misc.Unsafe 被移到 jdk.unsupported 模块——默认不导出。代码里直接用会报:
warning: Unsafe is internal proprietary API and may be removed in a future release
JDK 17+ 进一步收紧——访问会触发 Illegal reflective access 警告,未来版本可能直接禁止。用户代码必须迁移到 VarHandle。
JDK 内部依然在用 Unsafe——AtomicXxx 在 JDK 8 是 Unsafe,JDK 9+ 改为 VarHandle,但 Unsafe 类本身仍保留以兼容遗留代码。
# 7.2 VarHandle 在 Atomic 中的落地
回看 §3.2 AtomicInteger 的源码:
private static final VarHandle VALUE;
static {
MethodHandles.Lookup l = MethodHandles.lookup();
VALUE = l.findVarHandle(AtomicInteger.class, "value", int.class);
}
public final boolean compareAndSet(int expectedValue, int newValue) {
return VALUE.compareAndSet(this, expectedValue, newValue);
}
2
3
4
5
6
7
8
9
VarHandle 提供与 Unsafe 等价的能力,但:
- 类型安全(编译期检查)
- 受模块封装保护(不能跨模块访问私有字段)
- 提供分级内存语义(§7.3)
# 7.3 与 31 篇的衔接:四级访问模式
31 篇 §5.2 已详细讲过 VarHandle 四级访问模式,这里只做与 CAS 的串联:
| 模式 | 性能 | 用法示例 | 何时选 |
|---|---|---|---|
| plain | 最快 | vh.get(o) | 单线程 / 已有外部同步 |
| opaque | 同上 | vh.getOpaque(o) | 仅需要原子但无内存可见性需求(如计数) |
| acquire/release | 中 | vh.getAcquire(o) / vh.setRelease(o, v) | 类似 volatile 半屏障,单写多读场景 |
| volatile | 最慢 | vh.getVolatile(o) / vh.compareAndSet(...) | 完整 happens-before 语义 |
CAS 操作天然是 volatile 语义——compareAndSet 总是带完整内存屏障,不能降级。weakCompareAndSet 可以失败重试但不丢一致性。
# 7.4 Unsafe → VarHandle 迁移指南
// ❌ JDK 8 老代码
private static final long VALUE_OFFSET;
static {
VALUE_OFFSET = unsafe.objectFieldOffset(MyClass.class.getDeclaredField("value"));
}
boolean ok = unsafe.compareAndSwapInt(this, VALUE_OFFSET, expect, update);
// ✅ JDK 9+ 等价代码
private static final VarHandle VALUE;
static {
VALUE = MethodHandles.lookup().findVarHandle(MyClass.class, "value", int.class);
}
boolean ok = VALUE.compareAndSet(this, expect, update);
2
3
4
5
6
7
8
9
10
11
12
13
迁移收益:
- 同样的字节码层零开销(VarHandle 也是 intrinsic)
- 类型安全 + 模块友好
- 4 级语义可选(opaque/acquire/release/volatile)
唯一缺点:VarHandle 必须 static final 才能享受 JIT 优化(31 篇 §8.3 讲过原因)——稍微多一点样板代码。
# 8. 性能实证
# 8.1 JMH 测试场景
环境:32 核 / 64 线程 / JDK 21 / 临界区 = 自增 1 次
对比:synchronized / AtomicLong / LongAdder / DoubleAdder
线程数:1 / 16 / 64
指标:吞吐 (M ops/s)
2
3
4
# 8.2 单线程基线
吞吐 (M ops/s)
synchronized 120
AtomicLong 280 ← 单线程 CAS 比锁快 2 倍多
LongAdder 250 ← 比 AtomicLong 略慢(多一次 cells null 检查)
2
3
4
关键观察:单线程下 LongAdder 反而比 AtomicLong 慢 —— 没竞争时分段累加只是浪费。
# 8.3 16 线程高竞争
吞吐 (M ops/s)
synchronized 8 ← 全部串行化
AtomicLong 45 ← 大量 CAS 失败重试
LongAdder 430 ← 接近线性扩展 ⚡
2
3
4
LongAdder 此时已经爆杀 AtomicLong 近 10 倍。AtomicLong 因为 cache line 抢夺,吞吐随线程数增加反而下降。
# 8.4 64 线程极端竞争
吞吐 (M ops/s)
synchronized 5
AtomicLong 28 ← 恶化
LongAdder 1250 ★★★ 仍线性扩展 ★★★
2
3
4
结论:竞争越激烈,LongAdder 优势越明显——这就是它存在的全部理由。
# 8.5 选型决策树
是否需要"原子操作"?
├── 否 → 普通变量 + volatile
│
└── 是
│
竞争程度?
├── 单线程 / 极低竞争(< 4 线程)
│ └── AtomicLong / AtomicInteger(最简单)
│
├── 中等竞争(4 ~ 16 线程)
│ │
│ 需要精确读取吗?
│ ├── 需要 → AtomicLong
│ └── 最终一致即可 → LongAdder
│
└── 高竞争(> 16 线程)
│
一定用 LongAdder!
│
需要自定义运算?
├── 是 → LongAccumulator
└── 否 → LongAdder
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
额外维度:
- 对象引用 → AtomicReference(防 ABA → AtomicStampedReference)
- 数组元素 → AtomicXxxArray(注意伪共享)
- 改造已有类字段 → AtomicXxxFieldUpdater
- JDK 9+ 自定义无锁数据结构 → 直接用 VarHandle
# 9. 综合回扣与设计哲学
# 9.1 案例真相揭晓
① §1.1 AtomicLong 32 核翻车的真相:CAS 不是"零成本无锁"——LOCK CMPXCHG 触发 RFO + 全核 Invalidate 广播,竞争激烈时退化为"软件锁"。根治:换成 LongAdder,分段累加 + @Contended 防伪共享,吞吐恢复线性可扩展。
② §1.2 ABA 转账幽灵的真相:CAS 比的是地址相等(==),同一引用值的对象状态可能已"绕一圈"。根治:用 AtomicStampedReference,每次更新带版本戳,地址相同但戳不同时 CAS 失败重试。
③ 6 大追问全部作答:
| 追问 | 答案 | 章节 |
|---|---|---|
| ① CAS 凭什么原子 | LOCK CMPXCHG / LL+SC 单 CPU 指令 | §2.2-2.4 |
| ② Atomic 全家桶 | 基本/引用/数组/字段更新器四类,VarHandle 落地 | §3 |
| ③ ABA 防护 | StampedReference 版本戳 / MarkableReference 标志 | §4 |
| ④ LongAdder 凭什么快 10 倍 | Striped64 分段 + @Contended 伪共享防护 | §5 |
| ⑤ 为什么 VarHandle 取代 Unsafe | 模块化封装 + 类型安全 + 四级语义 | §6.5 + §7.1 |
| ⑥ AtomicLong vs LongAdder 选型 | 低竞争 AtomicLong,高竞争必 LongAdder | §8.5 |
# 9.2 三大设计哲学
1. "无锁三要素"——硬件原子 + 内存屏障 + 自旋重试:CAS 本身只是"原子比较交换",要构成完整的无锁算法需要三个支柱。
- 硬件原子:CPU 指令保证读-比-写不可分割(LOCK CMPXCHG / LL+SC)
- 内存屏障:保证 happens-before(volatile / acquire-release)
- 自旋重试:失败后重新读 → 重新计算 → 重新 CAS
少了任何一个,无锁都不成立。这也是为什么 31 篇 VarHandle 提供 acquire/release 等分级语义——不同算法需要不同强度的屏障,强行用 volatile 是性能浪费,用 plain 又可能不正确。
2. "分段思想"——化整为零的并发万能药:LongAdder 把单一热点拆成 N 个分段;ConcurrentHashMap 把全表锁拆成段锁(JDK 7)/ Node 锁(JDK 8);ForkJoinPool 把全局队列拆成 per-worker 队列;ReadWriteLock 把锁拆成读写两半。所有"高吞吐"并发数据结构都在做同一件事——降低共享数据的争用粒度。
回扣链:
- 卷一 09 篇 ConcurrentHashMap 分段
- 卷二 22 篇 Stream 并行 + ForkJoinPool 工作窃取
- 卷五 36 篇 AQS state 高低位编码(也是一种"分段")
- 本篇 LongAdder Cell 数组
- 卷六 47 篇 Reactor 多 EventLoop 模型(同思想)
3. "语义分级"——为正确性付出的代价应该按需:JDK 早期只有 volatile(最强语义),JDK 9 后通过 VarHandle 提供 plain/opaque/acquire-release/volatile 四级。强语义保证正确,但有性能代价;分级让用户在"够用就好"和"绝对正确"之间精确选择。这种思路:
- 数据库的事务隔离级别(READ COMMITTED → SERIALIZABLE)
- 缓存的一致性级别(最终一致 → 强一致)
- TCP 的可靠传输 vs UDP 的尽力而为
架构师的价值就在于"识别每段代码需要的最低语义级别"——多了浪费,少了出 bug。
# 9.3 速查表与下一篇预告
API 速查:
基本类型 引用类型
─ AtomicInteger ─ AtomicReference
─ AtomicLong ─ AtomicStampedReference ← ABA 防护
─ AtomicBoolean ─ AtomicMarkableReference
数组 字段更新器
─ AtomicIntegerArray ─ AtomicIntegerFieldUpdater
─ AtomicLongArray ─ AtomicLongFieldUpdater
─ AtomicReferenceArray ─ AtomicReferenceFieldUpdater
累加器(高竞争优化) 底层
─ LongAdder (求和) ─ Unsafe (JDK 内部)
─ LongAccumulator (任意运算) ─ VarHandle (用户首选)
─ DoubleAdder
─ DoubleAccumulator
2
3
4
5
6
7
8
9
10
11
12
13
14
15
死规矩:
1. 单线程 / 低竞争用 AtomicLong;高竞争(≥ 16 线程)必换 LongAdder
2. 涉及对象池 / 状态可绕回的场景必须 AtomicStampedReference 防 ABA
3. AtomicXxxArray 多线程访问相邻索引时注意伪共享
4. JDK 9+ 用户代码禁止 sun.misc.Unsafe,必须 VarHandle
5. VarHandle 必须声明为 static final 才能享受 JIT intrinsic 优化
6. LongAdder.sum() 是弱一致,对账等强一致场景必须配外部锁
2
3
4
5
6
🎯 下一篇预告:第 39 篇《五大同步器对比:CountDownLatch / CyclicBarrier / Semaphore / Exchanger / Phaser》——本篇拿下了"无锁原语"层,36 篇打通了 AQS 骨架、37 篇讲完了三把锁,下一篇把 AQS 的"应用层旗舰产品"一次讲透:CountDownLatch 一次性闸门(state 倒数 → 0 全开)+ CyclicBarrier 可循环屏障(基于 ReentrantLock + Condition 而非 AQS state)+ Semaphore 信号量(公平/非公平 + tryAcquireShared 源码)+ Exchanger 双向交换站(slot + spin + park 的精妙撮合)+ Phaser 阶段同步器(树形分层 + 动态注册 + tieredPhaser 源码)。一篇把 5 个工具横向对比 + 源码贯通 + 选型决策树 + 5 大典型业务场景案例(压测发令枪 / 多源数据汇聚 / 限流降级 / 生产者消费者交换 / 多阶段批处理)讲完。AQS 的"五大旗舰应用"一次讲透。