Java LongAdder详解与应用实战

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)方法是其核心 :

    1. 首先检查 cells数组是否已初始化。若未初始化,则尝试直接 CAS 更新 base字段。
    2. 若 CAS 更新 base失败(表明出现竞争),则进入冲突处理逻辑。
    3. 检查 cells数组是否已初始化、当前线程映射的 Cell槽位是否存在、以及尝试 CAS 更新该 Cell的值。
    4. 如果以上任意一步失败,则进入更复杂的 longAccumulate方法。该方法会处理 cells数组的初始化、扩容,以及为线程重新计算哈希值以寻找新的空闲槽位,确保更新最终能够完成 。

    sum()方法遍历 cells数组(如果已初始化),将所有非空 Cell的值与 base相加返回。由于此操作没有加锁,在并发更新时返回的是某个时刻的近似总值,具备最终一致性而非强一致性 。

🛠️ 实战应用示例

LongAdder非常适用于以下场景:

  1. API 请求统计与监控

    可以轻松统计服务的请求量、成功/失败次数、总耗时等指标 。

    java 复制代码
    public 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());
        }
    }
  2. 结合 ConcurrentHashMap 进行频率统计

    这是一种常见且高效的模式,用于统计元素出现次数 。

    arduino 复制代码
    ConcurrentMap<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();

⚠️ 使用注意事项与最佳实践

  1. 理解一致性语义LongAddersum()方法是最终一致性的。如果你的业务场景要求在任何时刻读取都必须是完全精确的值(例如金融账户余额),那么 AtomicLong或锁机制更为合适 。
  2. 关注内存占用Cell数组和避免伪共享的填充会带来比 AtomicLong更高的内存开销。在内存受限或并发度不高的环境中,需要权衡利弊 。
  3. 避免频繁调用 sum()sum()方法需要遍历 Cell数组,在数组较大时有一定开销。应避免在性能关键路径中频繁调用 。
  4. 重置操作reset()方法将 base和所有 Cell置零,但此操作非原子性。通常仅在确定没有并发更新时(如一个统计周期结束清零时)使用 。

🔄 选型指南:LongAdder vs. AtomicLong

场景 推荐选择 理由
极高并发写入,对读的实时精确性要求不高​(如统计、监控) LongAdder 写吞吐量极高,通过分散竞争避免性能瓶颈 。
低并发环境,或需要频繁读取精确瞬时值​(如序列号生成、状态标志) AtomicLong 读取 (get()) 是强一致性的单次 volatile 读,性能极高;接口丰富,支持 compareAndSet等复杂原子操作 。
需要复杂的累加操作​(如求最大值、最小值) LongAccumulator LongAdderLongAccumulator的一个特例(专用于加法)。LongAccumulator允许传入自定义二元运算符,功能更灵活 。

💎 总结

LongAdder是 Java 并发工具包中"分而治之"思想的杰出代表。它通过空间换时间,巧妙地化解了高并发下的写入竞争,在统计、监控等"写多读少"的场景下表现卓越。

选择 LongAdder的关键在于明确:​你是否愿意用读取操作的强一致性和更高的内存开销,来换取极高的并发写入性能

相关推荐
Lear5 小时前
Spring MVC 拦截器与过滤器的区别及执行顺序
后端
Lear5 小时前
SpringMVC之过滤器(Filter)
后端
WoodWall5 小时前
WebServer 02 Reactor模式
c++·后端
Tech有道5 小时前
滴滴面经分享:那个让我差点翻车的Spring面试题!
后端
再吃一根胡萝卜5 小时前
项目结构对比:Flask vs Django
后端
间彧5 小时前
LongAdder和AtomicLong的区别与使用场景选择
后端
间彧5 小时前
LongAdder支持高并发,为什么还要使用AtomicLong,直接使用LongAdder不就行了?(实则不然)
后端
WoodWall5 小时前
WebServer 00 重要前置知识
c++·后端
LCG元5 小时前
Docker容器化实战:将你的SpringBoot应用一键打包部署,告别环境不一致的烦恼!#第一部分
后端·docker