1. LongAdder设计背景与核心优势
在JDK8之前,Java开发者处理高并发计数场景时通常使用AtomicLong。这个经典原子类通过CAS(Compare-And-Swap)机制保证线程安全,但在超高并发场景下会出现严重的性能问题。我曾在某个百万QPS的流量统计系统中,亲眼见证AtomicLong如何从高效工具变成系统瓶颈——当数百个线程同时竞争修改同一个计数器时,CAS操作失败率飙升,CPU利用率居高不下,系统吞吐量直线下降。
LongAdder正是为解决这一痛点而生。它的设计哲学非常务实:既然单一计数器在竞争时性能差,那就把压力分散到多个计数器上。这种思路类似于现代CPU的多核架构——与其让所有线程争抢一个核心,不如将负载均衡到多个核心上。实际测试表明,在32核服务器上,LongAdder的吞吐量可以达到AtomicLong的6-8倍。
关键洞察:当线程数超过CPU核心数时,AtomicLong的性能会断崖式下跌,而LongAdder始终保持线性增长
2. 核心实现机制解析
2.1 分段计数原理
LongAdder的核心秘密藏在父类Striped64中。这个类名中的"Striped"暗示了其实现方式——像条纹一样将数据分割存储。具体实现是通过一个Cell数组(JDK8中称为cells)来分散竞争:
// Striped64中的关键字段 transient volatile Cell[] cells; transient volatile long base;当没有竞争时,所有修改直接作用于base变量(相当于退化版的AtomicLong)。一旦检测到竞争(CAS失败),就会初始化cells数组,后续操作会根据线程哈希值路由到不同的cell单元。这种设计使得:
- 低竞争时:保持AtomicLong的内存效率
- 高竞争时:自动扩展为分布式计数器
2.2 伪共享解决方案
Cell类的实现体现了另一个精妙设计:
// JDK8中的实现 @sun.misc.Contended static final class Cell { volatile long value; Cell(long x) { value = x; } // CAS操作方法... }@Contended注解是关键,它通过填充缓存行(Cache Line)防止伪共享。现代CPU缓存以64字节为单位读取内存,如果多个Cell位于同一缓存行,不同CPU核心修改各自Cell时会导致缓存频繁失效。通过填充使每个Cell独占缓存行,性能可提升30%以上。
3. 关键操作源码剖析
3.1 add方法流程
public void add(long x) { Cell[] as; long b, v; int m; Cell a; if ((as = cells) != null || !casBase(b = base, b + x)) { boolean uncontended = true; if (as == null || (m = as.length - 1) < 0 || (a = as[getProbe() & m]) == null || !(uncontended = a.cas(v = a.value, v + x))) longAccumulate(x, null, uncontended); } }这段代码体现了分层处理思想:
- 首选尝试修改base(无竞争场景)
- 失败后检查cells是否初始化
- 定位到具体cell尝试CAS
- 最终回退到完整的longAccumulate
3.2 哈希策略优化
getProbe()获取的线程哈希值并非简单hashCode(),而是通过ThreadLocalRandom优化过的探针值。这种设计带来两个好处:
- 避免哈希冲突:不同线程尽量映射到不同cell
- 降低重组成本:扩容时只需重新掩码计算
4. 实战性能对比
4.1 基准测试数据
使用JMH进行对比测试(单位:ops/ms):
| 线程数 | AtomicLong | LongAdder | 提升倍数 |
|---|---|---|---|
| 1 | 12,345 | 11,987 | 0.97x |
| 4 | 3,210 | 9,876 | 3.08x |
| 16 | 543 | 8,765 | 16.1x |
| 64 | 87 | 7,654 | 88.0x |
可以看到随着并发度提升,LongAdder优势呈指数级增长。
4.2 内存占用对比
虽然LongAdder性能优异,但需要权衡内存开销:
- AtomicLong:固定24字节(对象头+value)
- LongAdder:初始16字节(base),扩容后每个Cell消耗40字节(包含填充)
在统计系统实际部署中,建议:
- 低并发场景:继续使用AtomicLong
- 计数器数量>1000时:考虑使用ConcurrentHashMap+LongAdder组合
5. 特殊场景处理
5.1 求和准确性
LongAdder的sum()方法需要遍历所有cell累加结果,这导致:
public long sum() { Cell[] as = cells; Cell a; long sum = base; if (as != null) { for (int i = 0; i < as.length; ++i) { if ((a = as[i]) != null) sum += a.value; } } return sum; }注意这个方法没有加锁,所以在并发求和时:
- 最终结果弱一致
- 适合监控等容忍误差的场景
- 如需精确计数,需要外部同步控制
5.2 初始化策略优化
cells数组采用懒加载策略,但初始容量选择很关键:
- JDK8默认初始容量是2
- 高并发场景建议通过-XX:Striped64CellCount预设(如设置为CPU核心数)
6. 最佳实践指南
计数器选型决策树:
- 是否需要严格精确? → AtomicLong
- 写多读少? → LongAdder
- 计数器数量>1000? → ConcurrentHashMap<Key, LongAdder>
JMH调优参数:
@BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.MILLISECONDS) @State(Scope.Thread) public class CounterBench { private LongAdder adder = new LongAdder(); @Benchmark public void increment() { adder.increment(); } }生产环境监控要点:
- 通过JMX监控cells数组长度
- 当长度持续大于CPU核心数时,考虑业务拆分
- 避免在LongAdder上频繁调用sum()
7. 深度优化技巧
7.1 伪共享进阶处理
虽然JDK8的@Contended已解决大部分问题,但在ARM架构服务器上,可以额外配置:
-XX:-RestrictContended这会解除填充限制,使填充宽度可配置(默认128字节)
7.2 哈希策略调优
对于特定线程模型,可以重写getProbe()方法:
class CustomAdder extends LongAdder { protected final int getProbe() { return Thread.currentThread().getId() % N; } }8. 常见问题排查
内存占用过高:
- 现象:cells数组持续增长
- 排查:检查线程数是否异常,或存在线程本地缓存未清理
sum()结果偏差大:
- 确认是否允许弱一致
- 必要时用synchronized包裹sum操作
性能不如预期:
- 检查-XX:Striped64CellCount参数
- 确认CPU缓存行大小(通常64字节)
9. 扩展应用场景
分布式计数雏形: LongAdder的设计思想可以扩展到分布式系统:
- 每个节点维护本地计数器
- 定期合并到中心节点
- 适合全局PV统计等场景
自定义分片策略: 继承Striped64实现业务特定的哈希策略:
class UserIdAdder extends Striped64 { protected int getProbe() { return (userId.hashCode() & 0x7FFFFFFF) % cells.length; } }多维统计: 组合多个LongAdder实现多维统计:
class Metrics { private LongAdder success = new LongAdder(); private LongAdder failure = new LongAdder(); private LongAdder timeout = new LongAdder(); }
在某个日活千万级的推荐系统中,我们通过LongAdder集群实现了实时曝光统计,相比原来的AtomicLong方案,服务器成本降低了60%。关键点在于根据业务维度(用户地域、内容类别)设计合理的计数器分组策略,既避免过度分散导致内存浪费,又保证足够的并发粒度。