LongAdder 是 JUC 并发包中针对高并发计数场景设计的分段原子类。它通过在内部维护一个 base 变量与一组 Cell 数组,将原本集中在单个原子变量上的写竞争分散到多个独立内存单元上,从而显著降低热点变量在超高并发下的 CAS 失败率与线程自旋开销。

一、为什么 AtomicLong 在超高并发下会失效
AtomicLong 依靠 Unsafe 的 compareAndSwapLong 实现原子自增。在低频更新时,CAS 成功率很高,性能优于锁。但在秒杀、全局 QPS 统计等写热点场景中,成千上万个线程同时竞争同一个 value 的内存地址,只有一个线程能成功,其余线程不断重试,产生大量 CPU 空转。
我们可以用一段简单压测代码观察现象。假设有 64 个线程各累加一百万次,AtomicLong 的耗时往往是 LongAdder 的数倍。原因在于缓存一致性协议(如 MESI)中,同一缓存行的独占请求在多线程间频繁失效广播,导致总线风暴。这种竞争随线程数增加呈非线性恶化。
import java.util.concurrent.atomic.AtomicLong;
public class AtomicLongDemo {
private static final AtomicLong counter = new AtomicLong(0);
public static void main(String[] args) throws InterruptedException {
int threads = 64;
Thread[] ts = new Thread[threads];
for (int i = 0; i < threads; i++) {
ts[i] = new Thread(() -> {
for (int j = 0; j < 1_000_000; j++) {
counter.incrementAndGet();
}
});
}
long start = System.currentTimeMillis();
for (Thread t : ts) t.start();
for (Thread t : ts) t.join();
System.out.println("AtomicLong result=" + counter.get() + " cost=" + (System.currentTimeMillis() - start));
}
}
二、LongAdder 的分流核心:Cell 数组设计
LongAdder 继承自 Striped64,其核心思想是分而治之。它内部有一个 volatile Cell[] cells 数组,每个 Cell 是一个填充了伪共享保护的独立计数单元。线程通过 threadLocalRandomProbe 哈希值定位到某个 Cell,优先更新该槽位,而不是全局 base。
当 cells 为 null 且 CAS base 成功时,走低成本路径;一旦 CAS base 失败,说明存在竞争,LongAdder 会懒初始化 cells 数组,并为当前线程分配 Cell。后续该线程大多在自家 Cell 上累加,写冲突被限制在极少数的扩容或重散列场景。求和时 sum() 方法将 base 与所有非 null Cell 的值相加,得到最终一致视图。
import java.util.concurrent.atomic.LongAdder;
public class LongAdderDemo {
private static final LongAdder adder = new LongAdder();
public static void main(String[] args) throws InterruptedException {
int threads = 64;
Thread[] ts = new Thread[threads];
for (int i = 0; i < threads; i++) {
ts[i] = new Thread(() -> {
for (int j = 0; j < 1_000_000; j++) {
adder.increment();
}
});
}
long start = System.currentTimeMillis();
for (Thread t : ts) t.start();
for (Thread t : ts) t.join();
System.out.println("LongAdder result=" + adder.sum() + " cost=" + (System.currentTimeMillis() - start));
}
}
三、热点分流的底层机制剖析
3.1 哈希探测与槽位争用
线程首次进入累加逻辑时,若 cells 已存在,会用 probe 值按位与 (cells.length - 1) 计算索引。若该 Cell 为 null 或 CAS 失败,会调用 advanceProbe 扰动哈希并重试。这种线性探测让不同线程自然错开,避免所有线程挤在同一个 Cell。
如果连续多次探测都失败,说明当前槽位也是热点,LongAdder 会尝试扩容 cells 为两倍大小,并重新分配线程。扩容过程本身需要竞争一把自旋锁(cellsBusy),但发生频率极低,因此整体开销可控。
3.2 伪共享与 Cache 行填充
每个 Cell 内部用 @Contended 或手动 long p0~p6 填充,使一个 Cell 独占缓存行。否则相邻 Cell 在同一条缓存行时,线程 A 更新 Cell[0] 会令线程 B 的 Cell[1] 缓存失效,抵消分流收益。这种空间换时间的做法在高并发计数中至关重要。
// 简化版 Cell 结构示意(实际 JDK 使用 Unsafe 分配)
static final class Cell {
volatile long value;
// 缓存行填充,避免伪共享
long p0, p1, p2, p3, p4, p5, p6;
Cell(long x) { value = x; }
final boolean cas(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
}
}
四、使用时的注意事项与适用边界
LongAdder 的 sum() 不是原子快照,调用瞬间若有线程正在写,结果可能略低于真实值。因此它适合允许最终一致性的计数,如接口调用量、限流窗口计数,而不适合需要精确原子读写的余额字段。若业务要求每次读都绝对准确,仍应使用 AtomicLong 或加锁。
另外,LongAdder 在并发度很低时比 AtomicLong 多一次对象访问与分支判断,优势不明显。通常线程数超过 CPU 核心数且写操作密集时,分流收益才真正显现。实践中可结合监控,在 QPS 超过万级且出现 CAS 自旋告警时再引入。
| 对比维度 | AtomicLong | LongAdder |
|---|---|---|
| 竞争模型 | 全局单点 CAS | 分段 Cell 本地更新 |
| 高并发写吞吐 | 急剧下降 | 近似线性扩展 |
| 读取一致性 | 强一致 | 最终一致 |
| 内存占用 | 低 | 随并发略增 |
五、总结
LongAdder 通过分段原子类的设计,将超高并发下的热点变量写压力拆解到多个 Cell 中,以本地更新加最终汇总的方式换取吞吐量。理解其懒初始化、哈希探测、扩容与缓存行填充机制,能帮助我们在计数器选型时做出合理决策,在正确场景用对工具。