LongAdder分段原子类如何优化超高并发下的热点变量分流处理?
- 内容介绍
- 文章标签
- 相关推荐
本文共计741个文字,预计阅读时间需要3分钟。
LongAdder的核心价值在于其解决多线程环境下高效率计数问题的能力。它通过使用多个Cell来分担计数任务,减少了锁的使用,从而提高了并发性能。具体来说,LongAdder通过内部结构将计数任务分散到多个Cell中,每个Cell独立计数,最终将结果汇总,从而避免了传统计数器在并发环境下频繁的锁竞争。
为什么 AtomicLong 会在高并发下“堵车”
AtomicLong 所有线程都在争抢同一个内存地址。100 个线程同时调用 incrementAndGet(),大概率只有 1 个成功,其余全部自旋重试。CPU 花在空转上,吞吐反而断崖下跌。
- 现象:线程堆栈中反复出现 compareAndSwapLong 循环调用
- 本质:单点 CAS 竞争 → 失败率随线程数指数上升
- 后果:不是慢一点,是实际 QPS 崩溃,实测可能比 LongAdder 低 5–10 倍
LongAdder 是怎么实现分流的
它内部由一个 base 字段 + 一个 Cell[] 数组组成,线程通过自己的 threadLocalProbe 哈希值映射到某个 Cell 槽位,优先往那里写。
- 无竞争时:直接更新 base,零额外开销,行为和 AtomicLong 一致
- 首次冲突后:懒加载初始化 cells(默认长度 2),按 probe & (length−1) 定位槽位
- 后续同一线程:复用相同槽位,减少跨槽争用;不同线程大概率落在不同槽位
- 扩容保守:仅当多个线程反复冲突且数组未满时才扩容,避免小流量浪费
适用场景与关键边界
它不是 AtomicLong 的平替,而是一套为吞吐量专门设计的取舍方案。
- 适合:QPS 统计、错误计数、PV/UV 累加、日志打点等写多读少、允许毫秒级误差的监控类场景
- 不适合:库存扣减、金融对账、需要 compareAndSet 或 getAndIncrement 语义的逻辑
- sum() 返回的是近似和,遍历过程中其他线程仍在写入,结果反映的是尽力而为的当前状态
- reset() 和 sumThenReset() 都不阻塞写入,清零动作本身不是原子快照操作
使用时不能踩的坑
很多问题不是出在 API 不会用,而是忽略了它的运行机制。
- 别在每次请求里 new LongAdder —— 实例必须长期持有,比如 static final 或 Spring 单例 Bean
- 别把它放进 request scope 或 ThreadLocal —— 那就退化成单线程计数,完全浪费分段能力
- 按维度隔离计数(如按 API 路径)时,用 ConcurrentHashMap<String, LongAdder> 缓存实例,key 为路径名
- 避免在 Stream 的 lambda 里 new LongAdder —— 创建大量短命对象,失去分段意义
本文共计741个文字,预计阅读时间需要3分钟。
LongAdder的核心价值在于其解决多线程环境下高效率计数问题的能力。它通过使用多个Cell来分担计数任务,减少了锁的使用,从而提高了并发性能。具体来说,LongAdder通过内部结构将计数任务分散到多个Cell中,每个Cell独立计数,最终将结果汇总,从而避免了传统计数器在并发环境下频繁的锁竞争。
为什么 AtomicLong 会在高并发下“堵车”
AtomicLong 所有线程都在争抢同一个内存地址。100 个线程同时调用 incrementAndGet(),大概率只有 1 个成功,其余全部自旋重试。CPU 花在空转上,吞吐反而断崖下跌。
- 现象:线程堆栈中反复出现 compareAndSwapLong 循环调用
- 本质:单点 CAS 竞争 → 失败率随线程数指数上升
- 后果:不是慢一点,是实际 QPS 崩溃,实测可能比 LongAdder 低 5–10 倍
LongAdder 是怎么实现分流的
它内部由一个 base 字段 + 一个 Cell[] 数组组成,线程通过自己的 threadLocalProbe 哈希值映射到某个 Cell 槽位,优先往那里写。
- 无竞争时:直接更新 base,零额外开销,行为和 AtomicLong 一致
- 首次冲突后:懒加载初始化 cells(默认长度 2),按 probe & (length−1) 定位槽位
- 后续同一线程:复用相同槽位,减少跨槽争用;不同线程大概率落在不同槽位
- 扩容保守:仅当多个线程反复冲突且数组未满时才扩容,避免小流量浪费
适用场景与关键边界
它不是 AtomicLong 的平替,而是一套为吞吐量专门设计的取舍方案。
- 适合:QPS 统计、错误计数、PV/UV 累加、日志打点等写多读少、允许毫秒级误差的监控类场景
- 不适合:库存扣减、金融对账、需要 compareAndSet 或 getAndIncrement 语义的逻辑
- sum() 返回的是近似和,遍历过程中其他线程仍在写入,结果反映的是尽力而为的当前状态
- reset() 和 sumThenReset() 都不阻塞写入,清零动作本身不是原子快照操作
使用时不能踩的坑
很多问题不是出在 API 不会用,而是忽略了它的运行机制。
- 别在每次请求里 new LongAdder —— 实例必须长期持有,比如 static final 或 Spring 单例 Bean
- 别把它放进 request scope 或 ThreadLocal —— 那就退化成单线程计数,完全浪费分段能力
- 按维度隔离计数(如按 API 路径)时,用 ConcurrentHashMap<String, LongAdder> 缓存实例,key 为路径名
- 避免在 Stream 的 lambda 里 new LongAdder —— 创建大量短命对象,失去分段意义

