
上一篇用LongAdder统计请求次数。如果接下来想观察“一批请求里最大的耗时”只做加法就不够了。每个线程提供自己的样本再把这些样本合成一个最大值这正是LongAccumulator适合表达的统计需求。本文以Java 21为适用版本沿着“函数、初始值、并发更新、批次读取”展开。示例数据是人为构造的教学样本单位为毫秒不代表真实接口测量。一、从累加次数走向自定义合并可以把LongAccumulator看成一个带合并规则的统计容器调用accumulate(value)提交样本调用get()读取合并后的值。加法、最大值和最小值都能成为规则但规则必须适合拆分后再合并。例如三组耗时分别是120、360、24080、980、310450、220、170。无论先处理哪一组最终最大值都应该相同。我们关注的是这批样本的极值样本的提交顺序不会改变答案。二、函数和identity要一起选择构造器接受一个LongBinaryOperator和一个identity。统计最大值可以写成LongAccumulatormaximumnewLongAccumulator(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}};LongAccumulatormaximumnewLongAccumulator(Long::max,Long.MIN_VALUE);ListFuture?tasksnewArrayList();try(ExecutorServicepoolExecutors.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(maximummaximum.get());System.out.println(finishedBatchmaximum.getThenReset());System.out.println(emptymaximum.get());maximum.accumulate(-12);maximum.accumulate(-4);System.out.println(negativeBatchmaximum.get());}}javac LongAccumulatorDemo.javajavaLongAccumulatorDemo在JDK21.0.8中实际运行得到maximum980 finishedBatch980 empty-9223372036854775808 negativeBatch-4这里有两个值得留意的细节。首先Future.get()等待所有任务完成并把任务异常传回主线程读取结果时这个示例已经没有后台写入。其次getThenReset()后恢复的是构造时传入的identity所以empty显示Long.MIN_VALUE。空容器的值是一种初始化状态。实际统计服务可以另外记录样本数在没有样本时展示“暂无数据”避免把这个极小值当成真正的耗时。五、实时观察和精确批次各有边界get()返回的结果为非原子快照并发更新期间可能漏掉读取过程中新到达的更新。reset()与getThenReset()需要明确的静止点边写边清零不能保证精确划分批次。官方方法说明明确指出了这一点。需求更合适的处理方式观察多线程样本的最大值或最小值LongAccumulator配合合适函数与identity精确读取一批完成任务的极值等待任务结束再get或getThenReset仅计数或求和LongAdder语义更直接根据当前值批准配额、分配唯一序号使用AtomicLong、CAS或业务协调机制精确按时间窗口分桶先设计窗口交接与写入归属再选择统计容器LongAccumulator不会替你规定某个样本属于哪一分钟也不会把“检查阈值”和“批准动作”组合成事务。选型时先写清读取语义后面的实现会更稳妥。六、 思维导图LongAccumulator合并规则结合律与交换律函数无副作用初始值最大值用MIN_VALUE归零回到identity并发统计accumulate提交样本get合并读取使用边界读取非原子快照批次结束再清零七、总结总结要点合并规则决定适用范围。LongAccumulator适合可以拆分、重新分组和交换顺序的统计任务。最大值是一个直观的入口identity也要与规则匹配。读取时机决定结果含义。实时观察可以接受读数滞后精确批次需要先完成写入再读取和清零。示例里的等待步骤承担了这个边界。容器之外还有业务协议。样本数、窗口归属与配额决策需要单独设计不能从一个统计值直接推断协调操作已经安全。下一篇继续学习AtomicReference看看对象引用更新与不可变状态快照怎样配合。如果你觉得这篇文章对你有所帮助欢迎点赞、收藏、分享