LongAdder
是 Java 并发编程中为高并发计数而生的利器。下面这张表格能帮助你快速把握其全貌,之后我们再深入细节和实战。
特性维度 | 说明 |
---|---|
设计目标 | 高并发场景下的高性能计数/累加操作,优化多线程写竞争 。 |
核心思想 | 分而治之,分散热点 :将单一变量的竞争压力分散到多个单元(base + Cell[] ),以空间换取时间 。 |
关键数据结构 | base (基础值) 和 Cell[] (单元数组) 。 |
写入流程 | 1. 无竞争或低竞争时,CAS 操作直接更新 base 。 2. 竞争激烈时,线程通过哈希映射到 Cell[] 中的某个单元进行更新,大幅减少冲突 。 |
读取流程 (sum() ) |
返回 base 与所有 Cell 单元值的累加和。此操作不保证强一致性 ,是最终一致性的,因为在求和过程中可能有其他线程正在更新 。 |
主要优点 | 高并发写入性能远超 AtomicLong ,有效减少 CAS 空自旋,避免高竞争下的性能骤降 。 |
主要缺点 | 更高的内存消耗;读取操作 (sum() ) 非原子快照,是最终一致性的;不支持 compareAndSet 等原子条件更新操作 。 |
典型应用场景 | 高频统计计数器(如 API 调用次数、点击量)、监控指标收集、频率统计等"写多读少"且对读的实时精确性要求不高的场景 。 |
💡 深入核心原理
要理解 LongAdder
的高性能,需要深入其内部机制。
-
分散热点与动态扩容
初始时,所有线程都尝试通过 CAS 操作更新
base
变量。当并发加剧,某个线程 CAS 更新base
失败时,系统会初始化一个Cell
数组(默认大小为 2)。每个线程会根据其唯一的哈希值(探针,Probe)被映射到数组的某个槽位(Cell),然后对该槽位内的值进行更新 。随着竞争持续,如果线程在指定的Cell
上更新仍然失败,Cell
数组会进行扩容(通常翻倍),直到达到与 CPU 核数相当的水平,以进一步分散竞争 。这种设计将针对单一内存地址的激烈竞争,转化为对多个内存地址的相对平和的访问。 -
解决伪共享
Cell
类使用@sun.misc.Contended
注解进行填充,以避免伪共享 (False Sharing)。伪共享是指多个看似不相关的变量因位于同一个 CPU 缓存行中,当一个处理器更新其中一个变量时,会导致整个缓存行失效,其他处理器即使使用该行内的其他变量,也需要重新从内存加载,造成性能损失。@Contended
注解确保每个Cell
对象独立占据一个缓存行,从而提升缓存效率 。 -
核心方法流程
add(long x)
方法是其核心 :- 首先检查
cells
数组是否已初始化。若未初始化,则尝试直接 CAS 更新base
字段。 - 若 CAS 更新
base
失败(表明出现竞争),则进入冲突处理逻辑。 - 检查
cells
数组是否已初始化、当前线程映射的Cell
槽位是否存在、以及尝试 CAS 更新该Cell
的值。 - 如果以上任意一步失败,则进入更复杂的
longAccumulate
方法。该方法会处理cells
数组的初始化、扩容,以及为线程重新计算哈希值以寻找新的空闲槽位,确保更新最终能够完成 。
sum()
方法遍历cells
数组(如果已初始化),将所有非空Cell
的值与base
相加返回。由于此操作没有加锁,在并发更新时返回的是某个时刻的近似总值,具备最终一致性而非强一致性 。 - 首先检查
🛠️ 实战应用示例
LongAdder
非常适用于以下场景:
-
API 请求统计与监控
可以轻松统计服务的请求量、成功/失败次数、总耗时等指标 。
javapublic class ApiRequestMonitor { private final LongAdder requestCount = new LongAdder(); private final LongAdder totalLatency = new LongAdder(); public void recordRequest(long latency) { requestCount.increment(); totalLatency.add(latency); } public MonitoringSnapshot getSnapshot() { // 注意:sum() 获取的是瞬时近似值 return new MonitoringSnapshot(requestCount.sum(), totalLatency.sum()); } }
-
结合 ConcurrentHashMap 进行频率统计
这是一种常见且高效的模式,用于统计元素出现次数 。
arduinoConcurrentMap<String, LongAdder> freqMap = new ConcurrentHashMap<>(); public void count(String word) { // 如果键不存在,则原子性地放入一个新的 LongAdder freqMap.computeIfAbsent(word, k -> new LongAdder()) .increment(); // 然后递增 } // 获取某个词的频率 long frequency = freqMap.getOrDefault(word, new LongAdder()).sum();
⚠️ 使用注意事项与最佳实践
- 理解一致性语义 :
LongAdder
的sum()
方法是最终一致性的。如果你的业务场景要求在任何时刻读取都必须是完全精确的值(例如金融账户余额),那么AtomicLong
或锁机制更为合适 。 - 关注内存占用 :
Cell
数组和避免伪共享的填充会带来比AtomicLong
更高的内存开销。在内存受限或并发度不高的环境中,需要权衡利弊 。 - 避免频繁调用
sum()
:sum()
方法需要遍历Cell
数组,在数组较大时有一定开销。应避免在性能关键路径中频繁调用 。 - 重置操作 :
reset()
方法将base
和所有Cell
置零,但此操作非原子性。通常仅在确定没有并发更新时(如一个统计周期结束清零时)使用 。
🔄 选型指南:LongAdder vs. AtomicLong
场景 | 推荐选择 | 理由 |
---|---|---|
极高并发写入,对读的实时精确性要求不高(如统计、监控) | LongAdder | 写吞吐量极高,通过分散竞争避免性能瓶颈 。 |
低并发环境,或需要频繁读取精确瞬时值(如序列号生成、状态标志) | AtomicLong | 读取 (get() ) 是强一致性的单次 volatile 读,性能极高;接口丰富,支持 compareAndSet 等复杂原子操作 。 |
需要复杂的累加操作(如求最大值、最小值) | LongAccumulator | LongAdder 是 LongAccumulator 的一个特例(专用于加法)。LongAccumulator 允许传入自定义二元运算符,功能更灵活 。 |
💎 总结
LongAdder
是 Java 并发工具包中"分而治之"思想的杰出代表。它通过空间换时间,巧妙地化解了高并发下的写入竞争,在统计、监控等"写多读少"的场景下表现卓越。
选择 LongAdder
的关键在于明确:你是否愿意用读取操作的强一致性和更高的内存开销,来换取极高的并发写入性能。