目录
正在加载目录…
专栏文章
专栏文章
Java 专栏
1. Java:跨平台语言与生态全景 2. Java 并发:JMM、锁与线程池原理 3. JVM:类加载、内存、GC 与性能诊断 4. Java 性能调优:CPU、内存、锁与 IO 排查 5. Java 新特性速查:从 Lambda 到虚拟线程 6. Java 业务开发:高频陷阱与 Code Review 清单 7. Java 核心知识:集合、并发与 JVM 面试要点 8. AQS 原理:同步队列与加锁解锁全流程 9. JVM 与 Linux 内存:分区、分配与 GC 边界 10. AtomicLong 与 LongAdder:并发计数器选型 11. Java 引用与内存泄漏:六类场景与修复 12. Java 测试实践:JUnit、Mockito 与 Testcontainers 13. Java 并发入门:线程、锁与线程池全景

AtomicLong 与 LongAdder:并发计数器选型

发布于 2026-06-17 14:40 · 最后编辑于 2026-07-31 15:51 · 字数 1,959 👁 175 次阅读

本文从 LongAdder 的设计动机出发,结合 Striped64 源码与多平台 JMH 实测数据,分析两者在不同核数、不同竞争强度下的真实性能差异,并给出选型决策原则。

相关笔记Java 并发编程 · Java 业务开发陷阱 · Java 虚拟机

目录

章节说明
设计背景AtomicLong 的瓶颈与 LongAdder 的诞生
核心原理Striped64、Cell 数组、add() 路径
源码分析add() 的四条执行路径
性能实测macOS 12核 vs Linux 2核 JMH 数据
反直觉结论分析为什么 2 核机器上 AtomicLong 更快
选型决策一张表做决定

设计背景

AtomicLong 基于 CAS(LOCK CMPXCHG 或 ARM64 LDADD)实现原子递增。在单线程或低竞争场景下表现极佳,但在高并发多核竞争下存在结构性瓶颈:

8 个线程同时对同一 AtomicLong 做 CAS:
- 一次只有 1 个线程能成功
- 其余 7 个线程全部失败,自旋重试
- 越多核心同时竞争,失败率越高,CPU 越浪费

LongAdder(JDK 8,Doug Lea 设计)的核心思路:用空间换竞争

与其让所有线程争一个变量,不如让每个线程写自己的"格子",读的时候再把所有格子加起来。

核心原理

LongAdder 继承自 Striped64,内部维护两个字段:

// Striped64 的核心字段
volatile long base;      // 无竞争时的累加器(与 AtomicLong 等价)
volatile Cell[] cells;   // 有竞争后动态扩展的 Cell 数组
// Cell:每个 Cell 是一个对齐到 Cache Line 的独立 long
@sun.misc.Contended    // 防止伪共享(每个 Cell 独占 64 字节缓存行)
static final class Cell {
    volatile long value;
    boolean cas(long cmp, long val) {
        return VALUE.weakCompareAndSetRelease(this, cmp, val);
    }
}

@Contended 确保每个 Cell 独占一条 Cache Line,避免不同线程写不同 Cell 时因共享缓存行而产生伪共享(false sharing)。

整体逻辑:

flowchart TD
    A["longAdder.increment()"] --> B{cells 已初始化?}
    B -->|否| C{CAS base 成功?}
    C -->|是| Z["返回(快路径,≈ AtomicLong)"]
    C -->|否| D["longAccumulate()<br/>初始化 cells 或扩容"]
    B -->|是| E{CAS 对应 Cell 成功?}
    E -->|是| Z
    E -->|否| F["longAccumulate()<br/>rehash 换格子或扩容"]
    style Z fill:#cfc,stroke:#060
    style D fill:#fcc,stroke:#c00
    style F fill:#ff9,stroke:#960

关键设计:Cell 数组大小上限 = NCPU(物理 CPU 核数)。2 核机器上 cells.length 最大为 2。

源码分析

// java.util.concurrent.atomic.LongAdder#add(JDK 21)
public void add(long x) {
    Cell[] cs; long b, v; int m; Cell c;
    if ((cs = cells) != null                    // 路径 1:cells 已存在,跳过 base CAS
        || !casBase(b = base, b + x)) {         // 路径 2:CAS base 失败(竞争发生)
        boolean uncontended = true;
        if (cs == null || (m = cs.length - 1) < 0   // cells 为空
            || (c = cs[getProbe() & m]) == null      // 对应槽位为 null
            || !(uncontended = c.cas(v = c.value, v + x)))  // Cell CAS 失败
            longAccumulate(x, null, uncontended);    // 路径 3/4:初始化或扩容
    }
}

四条路径的代价对比:

路径触发条件指令数相对代价
base CAS 成功(无竞争)cells=null 且 CAS base 成功~3与 AtomicLong 相当
Cell CAS 成功cells 已初始化,对应 Cell CAS 成功~8稍高于 AtomicLong
longAccumulate(初始化)首次竞争时初始化 cells一次性开销
longAccumulate(rehash/扩容)Cell 竞争时换槽位或扩容低频触发

注意:一旦竞争发生(cells != null),即便后续竞争消失,每次 add() 都走路径二(先检查 cells),比 AtomicLong 多 ~5 条指令的固定开销。

性能实测

测试设备:macOS Apple M3 Pro(12核 ARM64)、Linux 腾讯云 CVM(2核 Intel x86_64)

JMH 配置:@GroupThreads(8),Warmup 3×1s,Measurement 5×1s,Fork 1,附 -prof gc

macOS(12核)Linux(2核)
AtomicLong39.658 ops/us152.975 ops/us
LongAdder1577.083 ops/us137.806 ops/us
LongAdder/AtomicLong+3879%(快 39.7 倍)−10%(慢 1.1 倍)
GC 分配0 B/op(两者相同)0 B/op(两者相同)

反直觉结论分析

为什么 macOS 12 核上 LongAdder 快 39.7 倍?

12 个物理核心可以同时运行 8 个线程,每个线程写自己的 Cell,物理上真正并行,CAS 失败率趋近 0。

macOS 上 8 线程的理想状态:
Thread-0 → Cell[0]   ← 几乎不竞争
Thread-1 → Cell[1]   ← 几乎不竞争
...(每线程独占一条 Cache Line)

为什么 Linux 2 核上 AtomicLong 反而更快?

关键认知:2 核机器上,"高并发"是假象。

Linux 2核 + 8线程的实际状态:
任意时刻只有 2 个线程在物理 CPU 上运行
OS 调度器把 8 个线程轮转到 2 个核上

在物理只有 2 个并发线程的情况下:

  1. Cell 数组的分散优势消失cells.length ≤ NCPU = 2,最多 2 个槽位,8 个线程轮流竞争同样 2 个 Cell,实际 CAS 冲突率与 AtomicLong 持平

  2. 固定指令开销变成负担:每次 add() 需要检查 cells != null、计算 getProbe() & m、做数组寻址,约 8 条指令。而 AtomicLong.incrementAndGet() 在 x86 上就是一条 LOCK XADD。在没有竞争优势可以摊薄的情况下,多出的 ~5 条指令直接体现在吞吐量差距上

  3. 量化验证

Linux(2核,8线程)
AtomicLong 每线程每 op~52 ns(1条 LOCK XADD + 调度开销)
LongAdder 每线程每 op~58 ns(8条指令 + 1条 CAS + 调度开销)
差距来源纯指令数差异,与竞争无关

选型决策

维度AtomicLongLongAdder
低竞争 / 单线程✅ 推荐(简单直接)⚠️ 可用,开销略高
高并发 + 核数 ≥ 8⚠️ CAS 竞争激烈首选(Cell 分散,吞吐高数十倍)
高并发 + 核数 ≤ 4✅ 通常更优⚠️ Cell 优势不显著,指令开销拖累性能
需要精确原子快照✅(get() 是精确值)❌(sum() 非原子快照)
需要 CAS 条件更新✅(compareAndSet()❌(不支持)
统计计数器(QPS、访问量)⚠️ 可用首选(不需要精确快照)

一句话决策原则

  • 服务器 8 核以上 + 多线程计数 → LongAdder
  • 需要读精确值 / 核数少 / 低并发 → AtomicLong

参考资料

← 返回列表

评论 (0)

暂无评论,来留下第一条吧。
登录注册 后才能发表评论