【JUC】原子类 LongAdder

LongAdder 是 JDK 8 引入的高并发计数器,核心思想是用空间换时间 :把对单个 value 的 CAS 竞争,分散到多个 Cell 槽位上,写多读少场景下吞吐量远高于 AtomicLong,代价是内存占用更高、sum() 不是原子快照。


一、为什么需要 LongAdder:AtomicLong 的瓶颈在哪

AtomicLong 内部只有一个 volatile long value。每次 incrementAndGet() 都是对这个单一变量的 CAS 自旋。低并发时没问题;高并发时,大量线程同时 CAS 同一个地址,失败线程不断自旋重试,竞争越激烈,CAS 失败率越高,自旋越多,CPU 空转越严重。

LongAdder 的解法很直观:既然一个热点变量抢不过来,那就拆成多个变量,让不同线程去更新不同的槽位,最后求和时再把结果汇总。

Oracle 官方文档明确说明了适用边界:当多个线程更新一个用于统计目的 的公共总和时,LongAdder 通常优于 AtomicLong;低竞争下两者性能相近,高竞争下 LongAdder 吞吐量显著更高,但空间消耗更大。


二、核心数据结构(源码级)

LongAdder 本身很简单,关键逻辑继承自 Striped64。核心字段就三个:

java 复制代码
// Striped64 中的关键字段
transient volatile Cell[] cells;   // 分散槽位数组,长度为 2 的幂
transient volatile long base;      // 无竞争时的基础值
transient volatile int cellsBusy;  // 自旋锁标记:0 空闲,1 占用

Cell 是内部类,本质是一个填充过的 volatile long,避免伪共享:

java 复制代码
@sun.misc.Contended  // 缓存行填充,防止相邻 Cell 互相干扰
static final class Cell {
    volatile long value;
    Cell(long x) { value = x; }
    final boolean cas(long cmp, long val) {
        return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
    }
}

@Contended 的作用是让每个 Cell 独占缓存行,避免一个 Cell 的修改导致其他 CPU 核心的缓存行失效,这在高度并发下对性能至关重要。


三、add() 的执行流程:从 base 到 Cell 的降级策略

LongAdder.add(long x) 是理解整个机制的核心入口。其逻辑分三层:

第一层:先试 base(无竞争路径)

java 复制代码
public void add(long x) {
    Cell[] as; long b, v; int m; Cell a;
    if ((as = cells) != null || !casBase(b = base, b + x)) {
        // 进入这里说明两种情况之一:
        // 1. cells 已经创建过(曾经发生过竞争)
        // 2. CAS base 失败(当前存在竞争)
        ...
    }
}

如果 cells 尚未初始化,且 casBase 成功,直接返回。这是单线程或低竞争下的快速路径 ,和 AtomicLong 行为一致,几乎零额外开销。

第二层:定位 Cell 槽位并 CAS

进入 if 后:

java 复制代码
boolean uncontended = true;
int h = getProbe();  // 当前线程的 probe hash(ThreadLocalRandom 生成)
if (as == null || (n = as.length) < 1 ||
    (a = as[(n - 1) & h]) == null ||
    !(uncontended = a.cas(v = a.value, v + x)))
    longAccumulate(x, null, uncontended);

关键点:

  • (n - 1) & h 等价于对数组长度取模,但用位运算更快(因为 n 是 2 的幂)。
  • 如果线程命中的 Cell 为 null,或者对该 Cell 的 CAS 失败,就进入 longAccumulate 处理复杂情况。

第三层:longAccumulate 兜底

longAccumulate 负责:

  1. 初始化 cells 数组(首次竞争时,长度默认 2);
  2. 为 null 槽位创建新 Cell;
  3. 扩容 cells 数组(当某个槽位竞争激烈时,最多扩到 CPU 核数大小);
  4. 重新 hash(rehash)让线程换一个槽位再试。

整个过程通过 cellsBusy 自旋锁保护结构变更,但值更新仍然用 CAS,锁只用于数组/槽位的创建和扩容这类低频操作。


四、sum() 为什么不是原子快照

java 复制代码
public long sum() {
    long sum = base;
    Cell[] as = cells;
    if (as != null) {
        for (Cell a : as)
            if (a != null) sum += a.value;
    }
    return sum;
}

sum() 遍历所有 Cell 并累加,但遍历过程中可能有线程正在修改某个 Cell。因此官方文档明确指出:返回的值不是原子快照,无并发更新时准确,有并发更新时可能遗漏某些更新。

这也是 LongAdder 不适合"精确同步控制"场景的根本原因。它适合的是统计类计数:数值最终会稳定,中间过程允许轻微误差。


五、最佳实践

适合用 LongAdder 的场景

场景 说明
QPS/请求计数 每秒几十万次 increment(),写远多于读
指标采集 配合 Micrometer 等监控框架,定期 sum() 上报
高频统计 如 Dubbo 的调用统计、Spring Security 的会话计数
ConcurrentHashMap 频率统计 官方推荐的经典用法

官方文档给出的典型模式:

java 复制代码
ConcurrentHashMap<String, LongAdder> freqs = new ConcurrentHashMap<>();
freqs.computeIfAbsent(key, k -> new LongAdder()).increment();

不适合用 LongAdder 的场景

需要精确原子语义的场景,例如:

  • 生成唯一序列号;
  • 实现自旋锁或同步器;
  • 需要 getAndIncrement() 返回旧值的逻辑。

这些场景应该继续用 AtomicLong。LongAdder 的 increment() 返回 void,sum() 也不是快照,无法替代 AtomicLong 的原子读写语义。

性能与内存的权衡

高并发下 LongAdder 的 Cell 数组可能增长到 CPU 核数规模。每个 Cell 因 @Contended 填充会占用更多内存。如果你的计数器数量极多(例如数百万个 key 各配一个 LongAdder),内存开销需要认真评估。一个常见的折中策略是:只在热点计数上使用 LongAdder ,冷门计数用 AtomicLong 或普通 long 配合其他同步手段。

读取频率不能太高

每次 sum() 都要遍历整个 cells 数组。如果业务逻辑中每写几次就读一次 ,LongAdder 的优势会被抵消,甚至不如 AtomicLong。正确的使用节奏是:大量写,低频读(如监控系统每 10 秒抓取一次)。


六、一个常见的误区

reset() 和 sumThenReset() 在并发场景下不安全 。官方文档明确说明:reset() 只应在确定没有并发更新 时使用,因为它在本质上是 racy 的。如果业务需要"读取并清零"的原子操作,LongAdder 做不到,应该考虑用 AtomicLong 配合 CAS 循环,或者换用其他设计。

相关推荐
瑞got3 小时前
java使用CountDownLatch做并行查询
java·开发语言
谢尔登3 小时前
02_开发环境配置
java
学长毕设3 小时前
基于springboot的实验室设备管理系统
java·spring boot·后端
学长毕设4 小时前
基于springboot的社区养老监护系统的设计与实现
java·spring boot·后端
会编程的吕洞宾4 小时前
RAG检索质量怎么评估?3个指标+LLM裁判实战
java·开发语言·人工智能·后端
卢总霸气4 小时前
上海交大机动学院学位论文参考文献Zotero CSL 样式-2026
java
段一凡-华北理工大学4 小时前
高炉能源系统智能化:热风炉、煤气与极致能效~系列文章07:高炉煤气:产生规律与全厂用户平衡
java·前端·能源
咖啡八杯4 小时前
Redis 工具封装:RedisCache 对 Operations 体系的二次封装
java·数据库·redis·缓存·框架·springboot·若依
wno7044 小时前
Springboot国际化信息(i18n)解析
java·spring boot·后端