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;
}

读操作的问题

  1. 非原子快照:遍历 Cell[0] 后、读 Cell[1] 之前,其他线程可能修改了 Cell[0] 或 Cell[1],导致 sum 结果不精确
  2. 性能开销:如果 Cell 扩容到 16 或 32 个,sum() 就要读 16~32 次,而 AtomicLong.get() 只读 1 次
  3. 不能用于决策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() 遍历开销 + 不精确性成问题

六、原理简述

  1. 首次竞争时初始化 cells 数组(大小为 2)
  2. 通过 threadLocalRandomProbe 哈希到某个 Cell,对其做 CAS
  3. 若某个 Cell 竞争失败,尝试换一个 Cell(重新哈希)
  4. 若持续失败,触发扩容(翻倍),直到 CPU 核心数上限

七、一句话总结

写多读少 + 高并发 + 接受最终一致性 → LongAdder
需要精确瞬时值 + CAS 语义 + 低并发 → AtomicLong

本质就是把一把大锁(单个 CAS 热点)拆成多把小锁(Cell 数组),各自无锁操作,只在读取时汇总。这是 JDK 里 ConcurrentHashMap 分段锁思想的延续。

posted @ 2026-07-11 15:27  deyang  阅读(14)  评论(0)    收藏  举报