第三章 无锁数据结构:从AtomicInteger到LongAdder
说到Java里的无锁原语,我最早接触的是AtomicInteger。那时候刚做交易系统,心里想:这玩意儿比synchronized快多了吧?后来踩了坑才发现——没那么简单。
今天咱们就聊聊,从AtomicInteger到LongAdder,Java到底经历了什么。说白了,就是一场「锁竞争消除」的进化史。
3.1 AtomicInteger:CAS的起点
AtomicInteger的核心是CAS(Compare-And-Swap)。它用CPU的硬件指令保证原子性,不需要操作系统挂起线程。嗯,听起来很美。
但我在项目中遇到过一个问题:高并发下,AtomicInteger的性能会急剧下降。为什么会这样?
你想想看,CAS是乐观锁。它假设冲突很少,直接尝试更新。如果失败了,就自旋重试。但一旦线程数多了,大家都抢同一个变量——
核心问题:CAS自旋会导致大量CPU空转,这叫「忙等待」。线程越多,自旋次数越多,性能反而越差。
我举个例子:
// 高并发下的AtomicInteger
AtomicInteger counter = new AtomicInteger(0);
// 100个线程同时执行
for (int i = 0; i < 100; i++) {
new Thread(() -> {
for (int j = 0; j < 10000; j++) {
counter.incrementAndGet(); // CAS自旋
}
}).start();
}
这段代码跑起来,你会发现CPU占用率飙升。每个线程都在拼命重试,但真正成功的只有一个。其他99个都在浪费电。
我曾经踩过的坑:在交易系统的订单计数器里用了AtomicInteger,压测到5000TPS时,CPU直接飙到95%。后来换成LongAdder,同样压力下CPU降到30%。差距就是这么明显。
3.2 LongAdder:分而治之的艺术
LongAdder是Java 8引入的。它的思路很简单:把竞争分散。
怎么分散?
- 内部维护一个
Cell[]数组 - 每个线程映射到不同的Cell
- 更新时只操作自己的Cell
- 取值时把所有Cell的值加起来
说白了,就是把一个热点变量,拆成多个冷门变量。每个线程各玩各的,互不干扰。
个人习惯:只要是多线程统计场景,我首选LongAdder。比如交易系统的成交笔数、委托笔数、延迟统计——这些高频写入、低频读取的场景,LongAdder简直是神器。
看个对比:
| 特性 | AtomicInteger | LongAdder |
|---|---|---|
| 底层机制 | CAS自旋 | Cell数组 + CAS |
| 高并发写入 | 性能急剧下降 | 线性扩展 |
| 读取性能 | 快(直接取值) | 慢(需要求和) |
| 适用场景 | 低竞争、频繁读取 | 高竞争、频繁写入 |
3.3 核心逻辑:一张图看懂
我画了张图,帮你理解LongAdder的设计思想:
3.4 源码级别的对比
咱们看看LongAdder的add()方法核心逻辑:
public void add(long x) {
Cell[] as; long b, v; int m; Cell a;
// 1. 先尝试更新base(类似AtomicInteger)
if ((as = cells) != null || !casBase(b = base, b + x)) {
// 2. 如果base竞争失败,进入Cell数组
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[getProbe() & m]) == null ||
!(uncontended = a.cas(v = a.value, v + x)))
// 3. 如果Cell也竞争失败,扩容或重试
longAccumulate(x, null, uncontended);
}
}
我解释一下这段代码:
- 第一步:先尝试更新
base。如果没竞争,直接成功——跟AtomicInteger一样快。 - 第二步:如果
base失败了,说明有竞争。那就找自己的Cell去更新。 - 第三步:如果Cell也冲突了(比如哈希碰撞),就调用
longAccumulate去扩容或者重新哈希。
关键点:LongAdder不是完全替代AtomicInteger。它用「空间换时间」,内部维护了一个Cell数组。每个Cell本质上就是一个AtomicLong。所以内存占用会大一些。
3.5 实战选择指南
我建议你这样选:
- 低竞争场景(线程数 < 4):用AtomicInteger。简单、轻量、读取快。
- 高竞争场景(线程数 > 8):用LongAdder。写入性能线性提升。
- 需要精确计数的场景:比如交易系统的订单号生成,用AtomicLong。因为LongAdder的sum()不是强一致的。
- 统计类场景:比如QPS计数、延迟分布统计,无脑上LongAdder。
我个人的经验:在交易系统的行情处理模块里,我用LongAdder统计每秒收到的行情笔数。100万笔/秒的行情下,AtomicLong的CPU开销占5%,换成LongAdder后降到0.5%。这个优化太值了。
3.6 避坑指南
我曾经犯过一个错误:在需要强一致性的场景用了LongAdder。
比如交易系统的资金余额更新。我用LongAdder来累加,结果sum()的时候,其他线程还在写。读到的值总是差一点。嗯,这个坑踩得挺疼。
记住:LongAdder的sum()不是原子操作。它遍历Cell数组时,其他线程可能正在修改。所以它只保证最终一致性,不保证强一致性。
另外,LongAdder的reset()方法也要小心。它重置所有Cell,但如果你在重置的同时有其他线程在写,数据就丢了。我一般用sumThenReset(),它先求和再重置,相对安全。
好了,从AtomicInteger到LongAdder,本质就是「从单点竞争到分片竞争」的思路。这个思想在交易系统里无处不在——分库分表、分区队列、分片缓存,都是这个道理。