一、引言:AtomicLong 的"自旋"困境
在 LongAdder 出现之前,Java 高并发计数的标准答案是 AtomicLong。它通过 CAS(Compare-And-Swap) 实现了线程安全的原子性累加,在低并发场景下表现完美。
但 CAS 有一个致命的弱点------自旋 重试 。当竞争激烈时,大量线程同时 CAS 修改同一个 value,只有一个能成功,其余线程会反复重试,陷入不停的自旋中。线程越多,竞争越激烈,CAS 失败率越高,CPU 空转越严重,性能急剧下降。
LongAdder的核心思想 :分散热点 。将一个共享的value拆分成多个独立的"计数单元"(Cell),每个线程只操作自己分配到的那个单元,将竞争压力从"一点"分散到"多点"。
这个思想可以类比为超市收银 :AtomicLong 就像只有一个收银台,所有顾客排一条长队;LongAdder 则是开了多个收银台,顾客分散到不同柜台,大大减少排队等待。
二、核心设计思想:分段累加 + 分散竞争
LongAdder 的设计可以用一句话概括:无竞争时用 base ,有竞争时用 Cell[] 数组分摊压力。
2.1 数据结构
LongAdder 继承自 Striped64 抽象类,内部维护三个核心变量:
java
// 位于 Striped64 中
transient volatile Cell[] cells; // 计数单元数组,每个 Cell 独立计数
transient volatile long base; // 基础值,无竞争时使用
transient volatile int cellsBusy; // 自旋锁,0表示空闲,1表示锁定(用于创建/扩容 cells)
2.2 Cell 内部类:避免伪共享
Cell 是 Striped64 的内部类,每个 Cell 就是一个独立的计数器:
java
/**
* Cell 是 LongAdder 和 ConcurrentHashMap 中用于分段计数的核心单元。
* 每个 Cell 相当于一个独立的计数器,用于分摊并发更新压力。
*
* 类比:每个 Cell 是一个收款柜台,每个柜台有自己的账本,各自算账,最后汇总。
*/
@sun.misc.Contended // 关键注解:消除伪共享!
static final class Cell {
/**
* volatile 保证多线程间的可见性:
* 一个线程修改了 value,其他线程立即能看到最新值
*/
volatile long value;
Cell(long x) {
this.value = x;
}
/**
* CAS 原子更新:尝试把 value 从 cmp 更新为 val
* @param cmp 期望值(我期望 value 现在等于多少)
* @param val 新值(如果满足期望,我就改成这个)
* @return true 更新成功,false 说明被其他线程抢先修改了
*
* 类比:多个顾客同时给同一个柜台交钱,只有第一个成功,
* 后面的要重试(重新读取当前值再次尝试)。
*/
final boolean cas(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
}
}
@Contended 注解是性能的关键 。CPU 缓存以 64 字节的缓存行(Cache Line) 为单位加载数据。如果多个 Cell 位于同一缓存行,一个 Cell 的更新会导致整行失效,其他 Cell 的读取也要重新从内存加载------这就是伪共享(False Sharing) 。@Contended 让每个 Cell 独占缓存行,避免了这种性能损耗。
2.2.1什么是伪共享(False Sharing)?
先理解 CPU 缓存行(Cache Line):
CPU 读内存时,不是"要一个字节就读一个字节",而是"整块整块搬"。通常一块是 64 字节------这就是缓存行。比如你想读一个小数(8 字节),CPU 会一次性把这块 64 字节区域内的所有数据都拉到缓存里,因为 CPU 觉得你接下来很可能也会用到邻近的数据。
使用**@Contended** 是在强制每一个cell独占一个缓存行,避免多个cell挤在一起互相影响。
2.3 工作流程
LongAdder 的 add() 方法遵循三级策略:
|---------|------------------------------------|-----------------------------------------|
| 步骤 | 条件 | 操作 |
| 第一级 | cells == null 且 CAS 更新 base 成功 | 直接操作 base,与 AtomicLong 无异 |
| 第二级 | cells 已存在,线程找到对应 Cell 且 CAS 成功 | 更新该 Cell 的值 |
| 第三级 | 上述均失败(Cell 不存在或 CAS 失败) | 调用 longAccumulate():创建/扩容 cells 或重试 |
三、源码深度剖析
3.1 add() 方法:入口与三级策略
java
/**
* LongAdder 的核心方法:累加一个值
*
* 设计思想:先抢 base,抢不到就找自己的 Cell 加,Cell 也失败就进入复杂处理
*
* 类比:银行有 1 个普通柜台(base)和 N 个 VIP 窗口(cells),
* 每个顾客(线程)先尝试普通柜台,如果忙就去自己的专属窗口。
*/
public void add(long x) {
Cell[] cs; // cells 数组:多个分段计数器(VIP 窗口列表)
long b, v; // b: base 当前值,v: Cell 的当前值
int m; // cells 数组长度 - 1(用于位运算取模)
Cell c; // 当前线程对应的 Cell
/**
* ===== 第一级:尝试直接更新 base(普通柜台) =====
*
* 条件 1:(cs = cells) != null
* - 如果 cells 已经存在,说明之前发生过竞争,直接跳过 base,走分段逻辑
*
* 条件 2:!casBase(b = base, b + x)
* - 如果 cells == null,尝试 CAS 更新 base(普通柜台)
* - 成功 → 加完了,直接返回
* - 失败 → 说明有竞争,进入下一级
*
* 这里的 || 是短路的:只要 cells != null,casBase 根本不会执行
*/
if ((cs = cells) != null || !casBase(b = base, b + x)) {
// 进入这里说明:cells 已存在 或 更新 base 失败(出现竞争)
boolean uncontended = true; // 标记:是否没有竞争
/**
* ===== 第二级:尝试更新对应的 Cell(专属窗口) =====
*
* 条件 1:cs == null
* - cells 数组不存在(可能是初始化失败)
*
* 条件 2:(m = cs.length - 1) < 0
* - 数组长度为 0(边界情况)
*
* 条件 3:(c = cs[getProbe() & m]) == null
* - 当前线程探针值对应的 Cell 还没创建
* - getProbe():当前线程的哈希值(类似排队号)
* - getProbe() & m:用位运算取模,定位到数组中的某个位置
* - 如果这个位置是 null,说明还没给这个线程分配专属窗口
*
* 条件 4:!(uncontended = c.cas(v = c.value, v + x))
* - CAS 更新 Cell 失败(有其他线程也在改这个 Cell)
* - uncontended = false 表示发生了竞争
*/
if (cs == null || (m = cs.length - 1) < 0 ||
(c = cs[getProbe() & m]) == null ||
!(uncontended = c.cas(v = c.value, v + x))) {
/**
* ===== 第三级:进入复杂处理 =====
*
* 触发条件(四种情况任意一种):
* 1. cs == null:cells 数组不存在 → 需要初始化
* 2. 数组长度为 0:边界异常 → 需要修复
* 3. 当前线程的 Cell 为 null:没有专属窗口 → 需要创建
* 4. CAS 更新 Cell 失败:发生竞争 → 需要扩容或重新定位
*
* longAccumulate 负责:
* - 初始化 cells 数组
* - 创建当前线程的 Cell
* - 扩容 cells 数组(减少竞争)
* - 重置当前线程的探针值(重新定位)
*/
longAccumulate(x, null, uncontended);
}
}
}
关键点解读:
-
cells != null:只要cells数组已存在,说明之前发生过竞争,直接走分段逻辑 -
!casBase(...):如果cells == null,尝试 CAS 更新base;成功则结束,失败说明出现竞争 -
getProbe():获取当前线程的探针值(Probe) ------ 一个由ThreadLocalRandom生成的哈希值,用于定位Cell数组的槽位 -
cs[getProbe() & m]:用探针值对数组长度取模(位运算),找到当前线程对应的Cell
|-----------------------|-------------------------|------------------------------------------------|
| 关键点 | 技术含义 | 通俗理解 |
| cells != null | 只要 cells 数组存在,说明之前发生过竞争 | 银行只要开过 VIP 窗口,就说明以前人多过,新顾客就别去普通柜台排队了,直接去 VIP 区 |
| !casBase(...) | CAS 更新 base 失败,说明出现竞争 | 普通柜台有人,你抢不到,只能去 VIP 区 |
| getProbe() | 每个线程的哈希值 | 你的排队号,决定你去哪个 VIP 窗口 |
| csgetProbe() \& m | 用探针值对数组长度取模定位 Cell | 排队号对窗口数取余,决定你去几号窗口 |
3.2 longAccumulate():Cell 的创建、扩容与重试
longAccumulate() 是 Striped64 中的核心方法,处理 Cell 不存在、CAS 失败等复杂情况。
java
final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {
int h;
// 初始化线程探针(如果为0)
if ((h = getProbe()) == 0) {
ThreadLocalRandom.current(); // 强制初始化
h = getProbe();
wasUncontended = true;
}
boolean collide = false; // 是否发生碰撞(需要扩容的标志)
for (;;) {
Cell[] as; Cell a; int n; long v;
if ((as = cells) != null && (n = as.length) > 0) {
// === Case 1: cells 已存在 ===
if ((a = as[(n - 1) & h]) == null) {
// 当前线程对应的 Cell 为空 → 创建新 Cell
if (cellsBusy == 0) { // 锁空闲
Cell r = new Cell(x);
if (cellsBusy == 0 && casCellsBusy()) { // 尝试获取锁
// 将新 Cell 放入数组
if (as[(n - 1) & h] == null) {
as[(n - 1) & h] = r;
break;
}
cellsBusy = 0; // 释放锁
}
}
collide = false;
} else if (!wasUncontended) {
// CAS 已知失败,重置标志,下次循环重试
wasUncontended = true;
} else if (a.cas(v = a.value, v + x)) {
// 尝试 CAS 更新 Cell,成功则退出
break;
} else if (n >= NCPU || cells != as) {
// 数组长度已达到 CPU 核数,或数组已被其他线程扩容,不再扩容
collide = false;
} else if (!collide) {
// 发生碰撞,设置标志,下次循环考虑扩容
collide = true;
} else if (cellsBusy == 0 && casCellsBusy()) {
// 获取锁,进行扩容(容量翻倍)
if (cells == as) {
Cell[] rs = new Cell[n << 1];
System.arraycopy(as, 0, rs, 0, n);
cells = rs;
}
cellsBusy = 0;
collide = false;
continue;
}
// 更新探针值,尝试新的槽位
h = advanceProbe(h);
} else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
// === Case 2: cells 未初始化 → 初始化 ===
boolean init = false;
try {
if (cells == as) {
Cell[] rs = new Cell[2]; // 初始容量为 2
rs[h & 1] = new Cell(x);
cells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init) break;
} else {
// === Case 3: 无法获取锁 → 回退到更新 base ===
if (casBase(v = base, (fn == null) ? v + x : fn.applyAsLong(v, x)))
break;
}
}
}
核心逻辑解读:
-
初始化 :首次发生竞争时,创建长度为 2 的
cells数组 -
创建 Cell :当前线程的槽位为空时,在
cellsBusy锁保护下创建新Cell -
扩容 :当多个线程竞争同一个
Cell导致 CAS 失败时,会触发扩容,容量翻倍 ,最大不超过 CPU 核数 -
探针更新 :
advanceProbe()更新线程的探针值,让线程尝试映射到其他Cell,减少哈希冲突 -
锁机制 :
cellsBusy是一个自旋锁 (0空闲,1锁定),用于保护cells数组的创建和扩容
3.3 sum() 方法:最终一致性
java
public long sum() {
Cell[] as = cells;
long sum = base;
if (as != null) {
for (Cell a : as) {
if (a != null) {
sum += a.value;
}
}
}
return sum;
}
注意事项:
-
sum()是非原子性 的:遍历Cell数组时,可能有线程正在更新某些Cell的值 -
因此
sum()返回的是最终一致性的快照值,适合统计场景(如 QPS 统计),不适合需要精确中间值的场景 -
如果需要精确值,应使用
AtomicLong或加锁保护
3.4 reset() / sumThenReset():重置与读取
java
public void reset() {
Cell[] as = cells;
base = 0L;
if (as != null) {
for (Cell a : as) {
if (a != null) {
a.value = 0L;
}
}
}
}
public long sumThenReset() {
Cell[] as = cells;
long sum = base;
base = 0L;
if (as != null) {
for (Cell a : as) {
if (a != null) {
sum += a.value;
a.value = 0L;
}
}
}
return sum;
}
sumThenReset() 适用于周期性统计场景(如每分钟统计一次 QPS),读取后重置所有计数单元。
四、LongAdder vs AtomicLong:性能对决
|-----------|--------------------------|----------------------|
| 对比维度 | AtomicLong | LongAdder |
| 数据结构 | 单个 volatile long value | base + Cell[] 数组 |
| 竞争处理 | 所有线程 CAS 同一个变量 | 分散到不同 Cell,各自 CAS |
| 低并发性能 | 优秀(直接 CAS) | 接近(略有数组访问开销) |
| 高并发性能 | 急剧下降(大量自旋) | 稳定优异 |
| 内存占用 | 小 | 较大(以空间换时间) |
| 适用场景 | 需要精确中间值、低并发 | 统计计数、高并发 |
五、代码实战
5.1 基础用法:接口调用次数统计
java
import java.util.concurrent.atomic.LongAdder;
public class ApiCallCounter {
// 统计各接口的调用次数
private final LongAdder totalCalls = new LongAdder();
private final LongAdder successCalls = new LongAdder();
private final LongAdder failureCalls = new LongAdder();
public void recordCall(boolean success) {
totalCalls.increment(); // 等价于 add(1)
if (success) {
successCalls.increment();
} else {
failureCalls.increment();
}
}
public void printStats() {
System.out.println("总调用: " + totalCalls.sum());
System.out.println("成功: " + successCalls.sum());
System.out.println("失败: " + failureCalls.sum());
System.out.println("成功率: " +
(successCalls.sum() * 100.0 / totalCalls.sum()) + "%");
}
}
5.2 高并发场景:QPS 统计
java
import java.util.concurrent.atomic.LongAdder;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class QpsCounter {
private final LongAdder counter = new LongAdder();
private volatile long lastQps = 0;
/**
* 每次请求调用,增加计数
*/
public void increment() {
counter.increment();
}
/**
* 每秒统计一次 QPS,然后重置
*/
public void startReport() {
Executors.newSingleThreadScheduledExecutor()
.scheduleAtFixedRate(() -> {
long qps = counter.sumThenReset(); // 读取并重置
lastQps = qps;
System.out.println("当前 QPS: " + qps);
}, 1, 1, TimeUnit.SECONDS);
}
public long getLastQps() {
return lastQps;
}
}
5.3 配合 ConcurrentHashMap 使用
LongAdder 经常与 ConcurrentHashMap 配合,统计每个 key 的出现次数:
java
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.LongAdder;
public class WordCount {
private final ConcurrentHashMap<String, LongAdder> map = new ConcurrentHashMap<>();
/**
* 统计某个词的出现次数
*/
public void count(String word) {
// computeIfAbsent 保证原子性创建 LongAdder
map.computeIfAbsent(word, k -> new LongAdder()).increment();
}
public long getCount(String word) {
LongAdder adder = map.get(word);
return adder == null ? 0 : adder.sum();
}
public void printAll() {
map.forEach((word, adder) ->
System.out.println(word + ": " + adder.sum()));
}
}
六、最佳实践与避坑指南
✅ 推荐做法
-
统计场景优先选用:接口调用次数、QPS 统计、日志计数等
-
周期性读取用
sumThenReset():减少遍历开销 -
配合
ConcurrentHashMap使用:实现高效的分布式计数器 -
不要求精确中间值时使用 :
sum()是最终一致性
七、总结
|----------|---------------------------------------------------|
| 维度 | 设计精髓 |
| 核心思想 | 分段累加 + 分散竞争,将单一热点拆分为多个独立计数单元 |
| 数据结构 | base(无竞争)+ Cell[](有竞争),@Contended 消除伪共享 |
| 竞争策略 | 无竞争用 CAS 更新 base → 有竞争分散到 Cell → CAS 失败则扩容/重试 |
| 性能优势 | 高并发下性能远超 AtomicLong,吞吐量提升数倍 |
| 适用场景 | 高并发统计计数(QPS、调用次数、事件计数),不追求精确中间值 |
一句话总结 :
LongAdder用"空间换时间"的哲学,将AtomicLong的"单点竞争"转化为"多点分摊",在高并发统计场景下实现了质的性能飞跃。它是ConcurrentHashMap分段锁思想在计数器领域的完美复刻。