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

上一篇通过AtomicInteger与CAS理解了单个共享值的原子更新。当大量线程只想"记一次请求",共同修改一个计数器就容易形成竞争。统计场景还有另一种思路:把更新压力分散到多个累加单元,读取时再合并结果。

LongAdder从Java 8开始提供。本文以Java 21为适用版本,沿着"累加、汇总、归零"的顺序解释它的边界,并用一个可运行例子统计请求次数。

一、先确定计数器承担什么职责

计数器看起来都在加一,业务要求却可能不同。监控页面的请求累计数,通常允许一次读取稍晚反映正在发生的更新;库存扣减则必须把"还有库存"与"扣减成功"绑定起来。

统计型累加适合LongAdder。唯一序号、配额判断、有条件扣减等场景需要明确的单变量原子读改写语义,可以继续使用AtomicLong、CAS循环或更完整的协调机制。

想一想调用者需要什么:只是观察总量,还是要根据这个值批准下一步动作?这个问题往往比"哪一种计数器更快"更早决定选型。

二、多个累加单元怎样缓解竞争

LongAdder维护的总和可以分布在一个或多个变量中。更新竞争增加时,这组变量可能动态增长,sum()再把它们的值汇总。与始终集中修改一个共享值相比,分散更新能够减少热点竞争,同时增加空间开销。这个定位来自Java 21 LongAdder文档。

图中的多个计数盒表示累加单元,导师猫在读取时汇总。它们是帮助理解的示意,不表示每个线程永久拥有一个盒子,也不保证始终存在固定数量的单元。

不要把LongAdder理解成"每次更新都完全没有竞争"。分散之后仍会发生冲突;低竞争、小规模任务中,它也未必比AtomicLong更合适。本文只核对结果,不给出未经测量的吞吐量排名。

三、完整示例:等待写入结束后读取总量

保存为LongAdderDemo.java,使用JDK21编译运行:

java 复制代码
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.LongAdder;

public class LongAdderDemo {
    public static void main(String[] args) throws Exception {
        LongAdder requests = new LongAdder();
        List<Future<?>> tasks = new ArrayList<>();

        try (ExecutorService pool = Executors.newFixedThreadPool(4)) {
            for (int worker = 0; worker < 4; worker++) {
                tasks.add(pool.submit(() -> {
                    for (int i = 0; i < 25_000; i++) {
                        requests.increment();
                    }
                }));
            }
            for (Future<?> task : tasks) {
                task.get();
            }
        }

        long total = requests.sum();
        System.out.println("requests=" + total);
        if (total != 100_000) {
            throw new AssertionError("unexpected request count");
        }

        // 所有写入任务已经结束,这里才执行汇总并归零。
        long batch = requests.sumThenReset();
        System.out.println("batch=" + batch);
        System.out.println("after reset=" + requests.sum());
        if (batch != 100_000 || requests.sum() != 0) {
            throw new AssertionError("unexpected reset result");
        }
    }
}
shell 复制代码
javac LongAdderDemo.java
java LongAdderDemo

输出:

text 复制代码
requests=100000
batch=100000
after reset=0

本例已在JDK21.0.8编译运行。这里用Future.get()等待每项任务结束,并让任务异常传回主线程。读取与归零都发生在写入结束以后,所以可以验证最终总量和清零结果。它验证的是这个例子的正确性,没有模拟高竞争性能测试。

四、sum()提供汇总,不能充当原子快照

**sum()**不会把多个累加单元在同一瞬间冻结。读取期间仍有更新时,得到的总量可能没有包含计算过程中发生的某些更新;没有并发更新时,它能够返回准确结果。对应语义见sum()方法说明。

因此,下面的写法不能严格限制请求数量:

java 复制代码
if (requests.sum() < limit) {
    requests.increment();
    acceptRequest();
}

除了汇总本身的语义,"检查"和"累加"还属于两个操作。多个线程可能同时通过条件,让结果超过limit。额度控制应使用能够协调检查与更新的方案,例如CAS循环、Semaphore或数据库约束,具体取决于配额的作用范围。

监控读数与业务决策要分别设计。一次近实时观察可以容忍更新尚未完全反映;批准库存、名额或资金动作需要更严格的约束。

五、reset()与sumThenReset()需要安静的写入阶段

**reset()**适用于确认没有线程继续更新的时候;**sumThenReset()**适合批次计算之间的静止点。有写入同时发生时,不能把后者当作精准切分统计窗口的原子交换。方法限制可对照reset()与sumThenReset()文档。

持续接收请求的服务若每分钟调用一次sumThenReset(),并不会因此获得严格的"这一分钟恰好有哪些请求"边界。需要精确窗口时,要额外协调写入,例如按业务时间将事件计入明确的窗口桶,再安排桶的关闭与读取。

只是展示累计增长趋势时,可以保留累计值,再由监控系统计算相邻采样的差值。采样差值同样要说明时间范围和读取语义,不能冒充事务级账本。

六、按业务键统计,还要考虑键的生命周期

结合ConcurrentHashMap,可以为不同业务键维护计数:

java 复制代码
ConcurrentHashMap<String, LongAdder> counts = new ConcurrentHashMap<>();
counts.computeIfAbsent("GET /orders", key -> new LongAdder()).increment();
long observed = counts.get("GET /orders").sum();

示意代码需要导入java.util.concurrent.ConcurrentHashMap。ConcurrentHashMap负责键到计数器的并发访问,LongAdder负责统计累加;这不会让整个Map的遍历成为全局一致快照。Map的并发语义见Java 21 ConcurrentHashMap文档。

业务上还要控制键的数量。把完整URL、用户输入或无限增长的请求标识直接作为统计键,可能让Map长期膨胀。删除计数器时也要考虑仍持有旧对象的写入者,避免把"移除映射"误当作所有线程都停止更新。

需求 优先考虑
统计请求总量,读取允许近实时汇总 LongAdder
获取单个原子递增返回值 AtomicLong
额度检查与扣减必须一起成功 CAS循环、锁或事务
明确批次结束后汇总并归零 静止点上的sumThenReset
精确业务窗口与跨进程统计 窗口设计、持久化与一致性协调

七、🧠 思维导图

#mermaid-svg-wqPeOwhn4Eh3RTcm{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wqPeOwhn4Eh3RTcm .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wqPeOwhn4Eh3RTcm .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wqPeOwhn4Eh3RTcm .error-icon{fill:#552222;}#mermaid-svg-wqPeOwhn4Eh3RTcm .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wqPeOwhn4Eh3RTcm .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wqPeOwhn4Eh3RTcm .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wqPeOwhn4Eh3RTcm .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wqPeOwhn4Eh3RTcm .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wqPeOwhn4Eh3RTcm .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wqPeOwhn4Eh3RTcm .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wqPeOwhn4Eh3RTcm .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wqPeOwhn4Eh3RTcm .marker.cross{stroke:#333333;}#mermaid-svg-wqPeOwhn4Eh3RTcm svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wqPeOwhn4Eh3RTcm p{margin:0;}#mermaid-svg-wqPeOwhn4Eh3RTcm .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-wqPeOwhn4Eh3RTcm .cluster-label text{fill:#333;}#mermaid-svg-wqPeOwhn4Eh3RTcm .cluster-label span{color:#333;}#mermaid-svg-wqPeOwhn4Eh3RTcm .cluster-label span p{background-color:transparent;}#mermaid-svg-wqPeOwhn4Eh3RTcm .label text,#mermaid-svg-wqPeOwhn4Eh3RTcm span{fill:#333;color:#333;}#mermaid-svg-wqPeOwhn4Eh3RTcm .node rect,#mermaid-svg-wqPeOwhn4Eh3RTcm .node circle,#mermaid-svg-wqPeOwhn4Eh3RTcm .node ellipse,#mermaid-svg-wqPeOwhn4Eh3RTcm .node polygon,#mermaid-svg-wqPeOwhn4Eh3RTcm .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wqPeOwhn4Eh3RTcm .rough-node .label text,#mermaid-svg-wqPeOwhn4Eh3RTcm .node .label text,#mermaid-svg-wqPeOwhn4Eh3RTcm .image-shape .label,#mermaid-svg-wqPeOwhn4Eh3RTcm .icon-shape .label{text-anchor:middle;}#mermaid-svg-wqPeOwhn4Eh3RTcm .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-wqPeOwhn4Eh3RTcm .rough-node .label,#mermaid-svg-wqPeOwhn4Eh3RTcm .node .label,#mermaid-svg-wqPeOwhn4Eh3RTcm .image-shape .label,#mermaid-svg-wqPeOwhn4Eh3RTcm .icon-shape .label{text-align:center;}#mermaid-svg-wqPeOwhn4Eh3RTcm .node.clickable{cursor:pointer;}#mermaid-svg-wqPeOwhn4Eh3RTcm .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-wqPeOwhn4Eh3RTcm .arrowheadPath{fill:#333333;}#mermaid-svg-wqPeOwhn4Eh3RTcm .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wqPeOwhn4Eh3RTcm .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wqPeOwhn4Eh3RTcm .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wqPeOwhn4Eh3RTcm .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-wqPeOwhn4Eh3RTcm .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wqPeOwhn4Eh3RTcm .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-wqPeOwhn4Eh3RTcm .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wqPeOwhn4Eh3RTcm .cluster text{fill:#333;}#mermaid-svg-wqPeOwhn4Eh3RTcm .cluster span{color:#333;}#mermaid-svg-wqPeOwhn4Eh3RTcm div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-wqPeOwhn4Eh3RTcm .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-wqPeOwhn4Eh3RTcm rect.text{fill:none;stroke-width:0;}#mermaid-svg-wqPeOwhn4Eh3RTcm .icon-shape,#mermaid-svg-wqPeOwhn4Eh3RTcm .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wqPeOwhn4Eh3RTcm .icon-shape p,#mermaid-svg-wqPeOwhn4Eh3RTcm .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-wqPeOwhn4Eh3RTcm .icon-shape .label rect,#mermaid-svg-wqPeOwhn4Eh3RTcm .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wqPeOwhn4Eh3RTcm .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-wqPeOwhn4Eh3RTcm .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-wqPeOwhn4Eh3RTcm :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} LongAdder
统计定位
高频累加
观察总量
更新与读取
分散累加单元
sum汇总
语义边界
并发读取非原子快照
归零需要静止点
选型与管理
配额另行协调
控制统计键数量

八、总结

总结要点

LongAdder适合高频统计型累加。把更新压力分散到多个单元,再在读取时汇总,是理解它的主线。

读取与归零的边界决定了统计结果能怎样使用。sum()不提供并发原子快照,sumThenReset()也不能自行建立精确的业务窗口。

业务约束与生命周期需要额外设计。额度审批、唯一序号、窗口关闭和键的回收,各自有不同的一致性要求。

下一篇继续讨论LongAccumulator,看看累加规则从求和扩展到最大值等运算后,需要满足哪些条件。

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

相关推荐
凯哥Java1 小时前
写代码怎么避免逻辑漏洞?
java·开发语言·人工智能·自动化
Ivanqhz1 小时前
正余弦位置编码(Sinusoidal Positional Encoding)
java·linux·数据库
BD_Marathon2 小时前
消息对象中字段的说明
java·前端·python
zlpzlpzyd2 小时前
nacos的openapi地址
java
落魄实习生3 小时前
Agent Scope Java 2.x 系列【10】Middleware
java·开发语言·ai
ZealSinger3 小时前
Spring7 RestTemplate弃用迁移
java·spring·后端开发·restclient
朝朝辞暮i4 小时前
C++ 第 13 课:值传递 —— 为什么函数里改了,外面却没变?
java·c++·算法
弹简特4 小时前
【Java项目-企悦抽】16-抽奖模块01-获取活动完整信息接口实现
java·开发语言·状态模式·springboot
泡茶喝茶写代码4 小时前
A股量化数据工程:从 REST 接口到策略信号(第 1 篇):指数列表与实时行情接入
java·python·股票数据api·股票数据·股票数据api接口·股票api数据接口·股票量化数据接口