☰
Java进阶篇之LongAdder:把统计更新分散,再读取总和
2026/10/3 7:23:02 网站建设 项目流程

上一篇通过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{LongAdderrequests=newLongAdder();List<Future<?>>tasks=newArrayList<>();try(ExecutorServicepool=Executors.newFixedThreadPool(4)){for(intworker=0;worker<4;worker++){tasks.add(pool.submit(()->{for(inti=0;i<25_000;i++){requests.increment();}}));}for(Future<?>task:tasks){task.get();}}longtotal=requests.sum();System.out.println("requests="+total);if(total!=100_000){thrownewAssertionError("unexpected request count");}// 所有写入任务已经结束,这里才执行汇总并归零。longbatch=requests.sumThenReset();System.out.println("batch="+batch);System.out.println("after reset="+requests.sum());if(batch!=100_000||requests.sum()!=0){thrownewAssertionError("unexpected reset result");}}}
javac LongAdderDemo.javajavaLongAdderDemo

输出:

requests=100000 batch=100000 after reset=0

本例已在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,可以为不同业务键维护计数:

ConcurrentHashMap<String,LongAdder>counts=newConcurrentHashMap<>();counts.computeIfAbsent("GET /orders",key->newLongAdder()).increment();longobserved=counts.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,看看累加规则从求和扩展到最大值等运算后,需要满足哪些条件。

👉如果你觉得这篇文章对你有所帮助,欢迎点赞、收藏、分享!😊

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询