上一篇通过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,看看累加规则从求和扩展到最大值等运算后,需要满足哪些条件。
👉 如果你觉得这篇文章对你有所帮助,欢迎点赞、收藏、分享!😊