ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java进阶篇之LongAdder:把统计更新分散,再读取总和

Java进阶篇之LongAdder:把统计更新分散,再读取总和 上一篇通过AtomicInteger与CAS理解了单个共享值的原子更新。当大量线程只想“记一次请求”共同修改一个计数器就容易形成竞争。统计场景还有另一种思路把更新压力分散到多个累加单元读取时再合并结果。LongAdder从Java 8开始提供。本文以Java 21为适用版本沿着“累加、汇总、归零”的顺序解释它的边界并用一个可运行例子统计请求次数。一、先确定计数器承担什么职责计数器看起来都在加一业务要求却可能不同。监控页面的请求累计数通常允许一次读取稍晚反映正在发生的更新库存扣减则必须把“还有库存”与“扣减成功”绑定起来。统计型累加适合LongAdder。唯一序号、配额判断、有条件扣减等场景需要明确的单变量原子读改写语义可以继续使用AtomicLong、CAS循环或更完整的协调机制。想一想调用者需要什么只是观察总量还是要根据这个值批准下一步动作这个问题往往比“哪一种计数器更快”更早决定选型。二、多个累加单元怎样缓解竞争LongAdder维护的总和可以分布在一个或多个变量中。更新竞争增加时这组变量可能动态增长sum()再把它们的值汇总。与始终集中修改一个共享值相比分散更新能够减少热点竞争同时增加空间开销。这个定位来自Java 21 LongAdder文档。图中的多个计数盒表示累加单元导师猫在读取时汇总。它们是帮助理解的示意不表示每个线程永久拥有一个盒子也不保证始终存在固定数量的单元。不要把LongAdder理解成“每次更新都完全没有竞争”。分散之后仍会发生冲突低竞争、小规模任务中它也未必比AtomicLong更合适。本文只核对结果不给出未经测量的吞吐量排名。三、完整示例等待写入结束后读取总量保存为LongAdderDemo.java使用JDK21编译运行importjava.util.concurrent.ExecutorService;importjava.util.concurrent.Executors;importjava.util.concurrent.Future;importjava.util.ArrayList;importjava.util.List;importjava.util.concurrent.atomic.LongAdder;publicclassLongAdderDemo{publicstaticvoidmain(String[]args)throwsException{LongAdderrequestsnewLongAdder();ListFuture?tasksnewArrayList();try(ExecutorServicepoolExecutors.newFixedThreadPool(4)){for(intworker0;worker4;worker){tasks.add(pool.submit(()-{for(inti0;i25_000;i){requests.increment();}}));}for(Future?task:tasks){task.get();}}longtotalrequests.sum();System.out.println(requeststotal);if(total!100_000){thrownewAssertionError(unexpected request count);}// 所有写入任务已经结束这里才执行汇总并归零。longbatchrequests.sumThenReset();System.out.println(batchbatch);System.out.println(after resetrequests.sum());if(batch!100_000||requests.sum()!0){thrownewAssertionError(unexpected reset result);}}}javac LongAdderDemo.javajavaLongAdderDemo输出requests100000 batch100000 after reset0本例已在JDK21.0.8编译运行。这里用Future.get()等待每项任务结束并让任务异常传回主线程。读取与归零都发生在写入结束以后所以可以验证最终总量和清零结果。它验证的是这个例子的正确性没有模拟高竞争性能测试。四、sum()提供汇总不能充当原子快照**sum()**不会把多个累加单元在同一瞬间冻结。读取期间仍有更新时得到的总量可能没有包含计算过程中发生的某些更新没有并发更新时它能够返回准确结果。对应语义见sum()方法说明。因此下面的写法不能严格限制请求数量if(requests.sum()limit){requests.increment();acceptRequest();}除了汇总本身的语义“检查”和“累加”还属于两个操作。多个线程可能同时通过条件让结果超过limit。额度控制应使用能够协调检查与更新的方案例如CAS循环、Semaphore或数据库约束具体取决于配额的作用范围。监控读数与业务决策要分别设计。一次近实时观察可以容忍更新尚未完全反映批准库存、名额或资金动作需要更严格的约束。五、reset()与sumThenReset()需要安静的写入阶段**reset()**适用于确认没有线程继续更新的时候**sumThenReset()**适合批次计算之间的静止点。有写入同时发生时不能把后者当作精准切分统计窗口的原子交换。方法限制可对照reset()与sumThenReset()文档。持续接收请求的服务若每分钟调用一次sumThenReset()并不会因此获得严格的“这一分钟恰好有哪些请求”边界。需要精确窗口时要额外协调写入例如按业务时间将事件计入明确的窗口桶再安排桶的关闭与读取。只是展示累计增长趋势时可以保留累计值再由监控系统计算相邻采样的差值。采样差值同样要说明时间范围和读取语义不能冒充事务级账本。六、按业务键统计还要考虑键的生命周期结合ConcurrentHashMap可以为不同业务键维护计数ConcurrentHashMapString,LongAddercountsnewConcurrentHashMap();counts.computeIfAbsent(GET /orders,key-newLongAdder()).increment();longobservedcounts.get(GET /orders).sum();示意代码需要导入java.util.concurrent.ConcurrentHashMap。ConcurrentHashMap负责键到计数器的并发访问LongAdder负责统计累加这不会让整个Map的遍历成为全局一致快照。Map的并发语义见Java 21 ConcurrentHashMap文档。业务上还要控制键的数量。把完整URL、用户输入或无限增长的请求标识直接作为统计键可能让Map长期膨胀。删除计数器时也要考虑仍持有旧对象的写入者避免把“移除映射”误当作所有线程都停止更新。需求优先考虑统计请求总量读取允许近实时汇总LongAdder获取单个原子递增返回值AtomicLong额度检查与扣减必须一起成功CAS循环、锁或事务明确批次结束后汇总并归零静止点上的sumThenReset精确业务窗口与跨进程统计窗口设计、持久化与一致性协调七、 思维导图LongAdder统计定位高频累加观察总量更新与读取分散累加单元sum汇总语义边界并发读取非原子快照归零需要静止点选型与管理配额另行协调控制统计键数量八、总结总结要点LongAdder适合高频统计型累加。把更新压力分散到多个单元再在读取时汇总是理解它的主线。读取与归零的边界决定了统计结果能怎样使用。sum()不提供并发原子快照sumThenReset()也不能自行建立精确的业务窗口。业务约束与生命周期需要额外设计。额度审批、唯一序号、窗口关闭和键的回收各自有不同的一致性要求。下一篇继续讨论LongAccumulator看看累加规则从求和扩展到最大值等运算后需要满足哪些条件。如果你觉得这篇文章对你有所帮助欢迎点赞、收藏、分享
返回列表