LongAdder 详解
目录
LongAdder 详解
Java 8 引入的
java.util.concurrent.atomic包中的高性能并发累加器。
一、核心设计思想
分段累加(Striped Accumulation):将单一热点值拆分为多个 Cell(槽),每个线程尽量操作不同的 Cell,减少 CAS 竞争。
LongAdder
/ | \
Cell0 Cell1 Cell2 ...
\ | /
sum()
本质是空间换时间 + 分段锁思想,把热点分散,让 CAS 几乎每次都能成功。
二、AtomicLong vs LongAdder
竞争模型对比
AtomicLong:所有线程对一个 value 做 CAS 自旋。
Thread1 ──┐
Thread2 ──┼──→ [CAS 竞争同一个 value] → 高冲突时大量自旋重试
Thread3 ──┘
LongAdder:线程分散到多个 Cell 上各自累加。
Thread1 ──→ Cell[0] (CAS)
Thread2 ──→ Cell[1] (CAS) 几乎无冲突
Thread3 ──→ Cell[2] (CAS)
↓
sum() = Cell[0] + Cell[1] + Cell[2]
为什么不选 AtomicLong
CAS 在高并发下的致命缺陷——自旋开销:
CAS 失败 → 重新读值 → 再 CAS → 又失败 → 继续循环...
并发线程越多,CAS 失败率越高,CPU 空转越严重。假设 32 个线程同时自增,每次只有一个线程成功,其余 31 个全部自旋重试,吞吐量急剧下降。
对比总结
| AtomicLong | LongAdder | |
|---|---|---|
| 数据在哪 | 一个 volatile long value |
分散在 base + N 个 Cell |
| 写操作 | CAS 竞争一个值,高并发时自旋严重 | 分散到不同 Cell,竞争极低 |
| 读操作 | 直接读一次 value,O(1) |
遍历所有 Cell 累加,O(N) |
| 原子性 | 天然原子(64 位 volatile) | 不保证,sum() 非原子快照 |
| CAS 语义 | 支持 compareAndSet |
不支持 |
| 内存开销 | 小 | 较大(Cell 数组) |
| 低并发(<4 线程) | 更优 | 有额外开销 |
| 高并发(≥4 线程) | 吞吐量下降明显 | 吞吐量显著更高 |
三、读操作(sum)的代价
sum() 实现本质
sum() 不是读一个值,而是遍历求和:
// LongAdder.sum() 简化逻辑
public long sum() {
long sum = base; // 先取 base
Cell[] cs = cells;
if (cs != null) {
for (Cell c : cs) { // 遍历每个 Cell
if (c != null)
sum += c.value; // 累加
}
}
return sum;
}
读操作的问题
- 非原子快照:遍历 Cell[0] 后、读 Cell[1] 之前,其他线程可能修改了 Cell[0] 或 Cell[1],导致 sum 结果不精确
- 性能开销:如果 Cell 扩容到 16 或 32 个,
sum()就要读 16~32 次,而AtomicLong.get()只读 1 次 - 不能用于决策:
if (adder.sum() > threshold)这样的判断不可靠
LongAdder 的设计取舍
把代价从写操作转移到读操作——写的时候爽了(无竞争),代价是读的时候要遍历求和且结果不精确。
四、常用方法
LongAdder adder = new LongAdder();
adder.add(1); // 加 1
adder.increment(); // 自增 1
adder.decrement(); // 自减 1
adder.sum(); // 求和(遍历所有 Cell)
adder.longValue(); // 等价于 sum()
adder.sumThenReset();// 求和并重置为 0
adder.reset(); // 重置为 0
五、适用场景
| 场景 | 推荐 | 原因 |
|---|---|---|
| QPS 统计 | LongAdder | 写频繁,读不频繁,允许近似值 |
| 请求计数器 | LongAdder | 多线程高并发累加 |
| 频率限制器 | LongAdder | 滑动窗口计数,短暂延迟可接受 |
| 唯一 ID 生成器 | AtomicLong | 需要 incrementAndGet() 返回精确值 |
| CAS 条件更新 | AtomicLong | 需要 compareAndSet() 语义 |
| 低并发场景 | AtomicLong | 无额外内存开销,更简单 |
| 需要瞬时精确快照 | AtomicLong | get() 是精确值,sum() 不是 |
| 读写比接近 1:1 | AtomicLong | sum() 遍历开销 + 不精确性成问题 |
六、原理简述
- 首次竞争时初始化
cells数组(大小为 2) - 通过
threadLocalRandomProbe哈希到某个 Cell,对其做 CAS - 若某个 Cell 竞争失败,尝试换一个 Cell(重新哈希)
- 若持续失败,触发扩容(翻倍),直到 CPU 核心数上限
七、一句话总结
写多读少 + 高并发 + 接受最终一致性 → LongAdder
需要精确瞬时值 + CAS 语义 + 低并发 → AtomicLong
本质就是把一把大锁(单个 CAS 热点)拆成多把小锁(Cell 数组),各自无锁操作,只在读取时汇总。这是 JDK 里 ConcurrentHashMap 分段锁思想的延续。
浙公网安备 33010602011771号