DoubleAdder适合高并发、只写不读的计数场景,如请求量统计、埋点上报;不适用于强一致读取、减法运算或金融计算。

DoubleAdder 适合什么场景
它专为高并发、只写不读的计数累加设计,比如统计请求量、埋点上报、指标聚合。不是用来替代 double 计算或做精确金融运算的——它不保证实时一致性,也不支持减法或原子比较。
常见错误现象:DoubleAdder.sum() 在累加过程中反复调用,误以为能拿到“当前精确值”;其实它只是快照,可能漏掉正在写入的分段值。
- 适用:吞吐优先、允许短暂延迟、写远多于读(如每秒万级 increment)
- 不适用:需要强一致读取、要频繁做
sum() == x判断、涉及减法或除法中间态 - 和
AtomicDouble(非 JDK 原生)比,DoubleAdder在 contended 场景下性能通常高 3–10 倍
怎么正确初始化和累加
别在循环里反复 new,也别拿它当普通变量传参修改。它内部靠线程本地 cell 分片,初始化开销小,但滥用会破坏分片效果。
使用场景:Web 过滤器统计响应时间总和、Flink/Spark 中的 metrics collector、日志采样计数器。
立即学习“Java免费学习笔记(深入)”;
- 初始化直接
new DoubleAdder(),无需额外配置 - 累加统一走
add(double x),不要用increment()(那是整数用的) - 避免在 lambda 或 ForkJoinTask 里捕获并复用同一个
DoubleAdder实例,线程切换可能导致 cell 分配失效
DoubleAdder timerSum = new DoubleAdder(); // 正确:每个线程独立调用 timerSum.add(elapsedMs); // 错误:试图用 += 操作符(编译不过) // timerSum += elapsedMs;
sum() 的时机和代价
sum() 是唯一读操作,但它要遍历所有线程本地 cell 并加总,不是 O(1)。高并发下如果频繁调用,反而成为瓶颈,甚至引发 false sharing。
性能影响:在 64 核机器上,100 个活跃线程时,单次 sum() 可能耗时 50–200ns,而 add() 通常在 5–15ns。
- 只在必要时刻调用,比如定时上报、监控拉取、批次 flush
- 不要放在 hot path 循环里,尤其不能在每次 HTTP 请求结束时都
sum() - 如果需近似值,可考虑缓存 + 定期刷新,比如用 ScheduledExecutorService 每 5 秒更新一次
volatile double cachedSum
和 AtomicDouble、synchronized 对比的坑
很多人想当然认为 “AtomicDouble 更精确,所以更好”,其实错在混淆了语义:AtomicDouble 用 CAS 串行化所有写,争抢激烈时大量失败重试,吞吐暴跌;DoubleAdder 放弃强顺序换高吞吐,是正交设计。
容易踩的坑:
- 把
DoubleAdder当作AtomicDouble的升级版来用,结果发现sum()不稳定,怀疑是 bug - 在单线程环境硬套
DoubleAdder,反而比double局部变量慢 2–3 倍(无分片收益,纯开销) - 没注意精度问题:它底层用
long存放 double 的 bit 表示,累加极大量后仍存在浮点舍入误差,和普通 double 相加一致,不是额外缺陷
真正关键的是根据压测数据选型:QPS 超 5k 且写占比 >90%,DoubleAdder 几乎总是更优;否则老老实实用 synchronized 块包一个 double 字段更省心。











