doubleadder 更适合高频累加因其将操作分散到多个cell避免伪共享和总线竞争;synchronized和atomicdouble则集中于单内存地址易引发缓存行争用。

DoubleAdder 为什么比 synchronized 或 AtomicDouble 更适合高频累加
因为 DoubleAdder 不依赖单一共享变量,而是把累加操作分散到多个 Cell 上,每个线程优先往自己的 Cell 写,避免所有线程反复争抢同一缓存行。而 synchronized 块或 AtomicDouble 的 add 方法,底层都落在一个内存地址上——这在多核下极易触发伪共享和总线锁竞争。
关键点在于:不是“有没有锁”,而是“锁的粒度是否被压到单个缓存行以内”。DoubleAdder 的设计让多数写操作不跨线程、不跨缓存行,只有最终 sum() 时才做一次汇总。
初始化和使用时必须注意的两个对齐细节
DoubleAdder 本身不控制内部 Cell 的内存布局,但它的性能上限受限于 CPU 缓存行大小(通常是 64 字节)。如果多个 Cell 恰好落在同一缓存行里,仍会引发伪共享。
- Java 8+ 的
DoubleAdder已在Cell类中做了@sun.misc.Contended标记(需开启 JVM 参数-XX:-RestrictContended),但该标记仅对字段级填充有效,不能保证不同Cell实例之间隔离 - 若你手动管理
Cell数组(比如实现自定义累加器),必须确保每个Cell占用完整缓存行:用alignas(64)(C++)或字节填充(Java 中靠Unsafe+ padding 字段)对齐 - 不要复用同一个
DoubleAdder实例统计语义完全无关的指标(比如同时用于 QPS 和错误数),否则cells扩容逻辑可能因混合写入而误判竞争强度
sum() 调用时机直接影响缓存一致性开销
DoubleAdder.sum() 是非原子快照:它先读 base,再遍历所有非空 Cell 并累加。这个过程本身不加锁,但每次读 Cell.value 都是一次独立的 volatile 读 —— 在高并发写入未停歇时,可能导致多次缓存行失效。
- 避免在 tight loop 里频繁调用
sum(),比如每毫秒都取一次值用于日志打印 - 如需近实时统计,可考虑周期性(如每 100ms)用单独线程调用
sum()并缓存结果,其他线程读缓存值 -
sum()不保证强一致性:它不阻塞写入,所以返回值可能略低于“理论上应有值”,这是设计取舍,不是 bug
当 CPU 核心数远大于活跃线程数时,cells 可能闲置浪费
DoubleAdder 的 cells 数组大小默认不超过 CPU 核心数(通过 Runtime.getRuntime().availableProcessors() 获取),但实际活跃线程可能远少于该值。这时部分 Cell 永远不会被用到,反而增加 sum() 遍历开销。
- 没有标准 API 控制初始
cells大小,但可通过反射在构造后清空cells数组(不推荐生产环境) - 更稳妥的做法是:预估峰值并发线程数,在监控中观察
cells.length和cellsBusy状态,若长期cells.length == 1,说明根本没触发分片,直接用AtomicDouble更轻量 - 注意:JVM 启动参数
-XX:ActiveProcessorCount=N会影响availableProcessors()返回值,进而影响DoubleAdder的扩容策略
sum() 的调用频率与 Cell 实际利用率之间的错配——它不像锁那样“用了就慢”,而是在低竞争下悄悄退化为单点瓶颈,在高负载时又因遍历开销放大延迟。优化必须从线程行为模式出发,而不是只盯着类名换掉。










