上一篇用LongAdder统计请求次数。如果接下来想观察“一批请求里最大的耗时”,只做加法就不够了。每个线程提供自己的样本,再把这些样本合成一个最大值,这正是LongAccumulator适合表达的统计需求。
本文以Java 21为适用版本,沿着“函数、初始值、并发更新、批次读取”展开。示例数据是人为构造的教学样本,单位为毫秒,不代表真实接口测量。
一、从累加次数走向自定义合并
可以把LongAccumulator看成一个带合并规则的统计容器:调用accumulate(value)提交样本,调用get()读取合并后的值。加法、最大值和最小值都能成为规则,但规则必须适合拆分后再合并。
例如,三组耗时分别是120、360、240;80、980、310;450、220、170。无论先处理哪一组,最终最大值都应该相同。我们关注的是这批样本的极值,样本的提交顺序不会改变答案。
二、函数和identity要一起选择
构造器接受一个LongBinaryOperator和一个identity。统计最大值可以写成:
LongAccumulatormaximum=newLongAccumulator(Long::max,Long.MIN_VALUE);初始值参与合并,也用于归零后的恢复。对于最大值,Long.MIN_VALUE不会压过任何合法long样本;对于最小值,可以使用Long.MAX_VALUE;加法对应0。这个选择同时覆盖了负数样本。若把最大值初始值随手设为0,一组全为负数的样本就会得到错误结果。
合并函数需要满足三个要求:结合律允许改变分组,交换律允许改变顺序,无副作用允许重试时再次计算。官方Java 21 LongAccumulator文档说明了这些约束以及最大值的identity选择。
减法不满足这些条件。“把两个值相加再除以2”也不能直接用于平均值统计:先平均哪两个数,会影响最终结果。平均值可以分别收集总和与数量,再在明确的统计边界计算。
三、分散更新后怎样得到一个结果
在OpenJDK21的实现中,数据可能保存在base与多个Cell中,读取时使用同一个函数合并。这是实现层面的组织方式,单元数量和线程映射都无需由业务代码决定。可以结合OpenJDK21 LongAccumulator源码理解读取路径。
图中的几个样本盒表示可参与合并的统计单元。对最大值来说,合并局部最大值仍能得到整体最大值;图示不表示每个线程永久占有一个固定盒子。
四、完整示例:并发提交,批次结束后读取
保存为LongAccumulatorDemo.java,用JDK21编译运行:
importjava.util.ArrayList;importjava.util.List;importjava.util.concurrent.ExecutorService;importjava.util.concurrent.Executors;importjava.util.concurrent.Future;importjava.util.concurrent.atomic.LongAccumulator;publicclassLongAccumulatorDemo{publicstaticvoidmain(String[]args)throwsException{long[][]samples={{120,360,240},{80,980,310},{450,220,170},{600,90,700}};LongAccumulatormaximum=newLongAccumulator(Long::max,Long.MIN_VALUE);List<Future<?>>tasks=newArrayList<>();try(ExecutorServicepool=Executors.newFixedThreadPool(4)){for(long[]batch:samples){tasks.add(pool.submit(()->{for(longvalue:batch){maximum.accumulate(value);}}));}for(Future<?>task:tasks){task.get();}}System.out.println("maximum="+maximum.get());System.out.println("finishedBatch="+maximum.getThenReset());System.out.println("empty="+maximum.get());maximum.accumulate(-12);maximum.accumulate(-4);System.out.println("negativeBatch="+maximum.get());}}javac LongAccumulatorDemo.javajavaLongAccumulatorDemo在JDK21.0.8中实际运行得到:
maximum=980 finishedBatch=980 empty=-9223372036854775808 negativeBatch=-4这里有两个值得留意的细节。首先,Future.get()等待所有任务完成,并把任务异常传回主线程;读取结果时,这个示例已经没有后台写入。其次,getThenReset()后恢复的是构造时传入的identity,所以empty显示Long.MIN_VALUE。
空容器的值是一种初始化状态。实际统计服务可以另外记录样本数,在没有样本时展示“暂无数据”,避免把这个极小值当成真正的耗时。
五、实时观察和精确批次各有边界
get()返回的结果为非原子快照,并发更新期间可能漏掉读取过程中新到达的更新。
reset()与getThenReset()需要明确的静止点;边写边清零不能保证精确划分批次。官方方法说明明确指出了这一点。
| 需求 | 更合适的处理方式 |
|---|---|
| 观察多线程样本的最大值或最小值 | LongAccumulator配合合适函数与identity |
| 精确读取一批完成任务的极值 | 等待任务结束,再get或getThenReset |
| 仅计数或求和 | LongAdder语义更直接 |
| 根据当前值批准配额、分配唯一序号 | 使用AtomicLong、CAS或业务协调机制 |
| 精确按时间窗口分桶 | 先设计窗口交接与写入归属,再选择统计容器 |
LongAccumulator不会替你规定某个样本属于哪一分钟,也不会把“检查阈值”和“批准动作”组合成事务。选型时先写清读取语义,后面的实现会更稳妥。
六、🧠 思维导图
七、总结
总结要点
合并规则决定适用范围。LongAccumulator适合可以拆分、重新分组和交换顺序的统计任务。最大值是一个直观的入口,identity也要与规则匹配。
读取时机决定结果含义。实时观察可以接受读数滞后;精确批次需要先完成写入,再读取和清零。示例里的等待步骤承担了这个边界。
容器之外还有业务协议。样本数、窗口归属与配额决策需要单独设计,不能从一个统计值直接推断协调操作已经安全。
下一篇继续学习AtomicReference,看看对象引用更新与不可变状态快照怎样配合。
👉如果你觉得这篇文章对你有所帮助,欢迎点赞、收藏、分享!😊