多个线程只做累加统计时,AtomicLong 会让所有更新竞争同一个值。Java 8 提供了 LongAdder,把部分竞争分散到不同位置,最后再求和。
一个计数例子
import java.util.concurrent.atomic.LongAdder;
public class LongAdderDemo {
public static void main(String[] args) throws InterruptedException {
LongAdder count = new LongAdder();
Thread[] threads = new Thread[4];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 10000; j++) count.increment();
});
threads[i].start();
}
for (Thread thread : threads) thread.join();
System.out.println(count.sum());
}
}输出 40000。这里在所有更新线程结束后才读取结果,所以没有并发写入干扰求和。
LongAdder 的 increment() 不返回每次递增后的唯一值。如果要生成序列号,需要 AtomicLong.incrementAndGet() 那样的语义,不能直接替换。
base 和 cells
看 JDK 8 源码,LongAdder 继承了 Striped64,主要使用一个 base 和一个 Cell 数组。
竞争较小时,先尝试 CAS 更新 base。发生竞争后,可以初始化 cells,根据线程的探针值选择其中一个单元,再尝试更新。单元竞争时会调整探针,必要时扩容,结构变化由额外的控制字段协调。
最终结果是 base 加上各个非空 Cell 中的值。它不是“一个线程固定对应一个 Cell”,也不是完全没有竞争;只是把集中在一个位置的竞争摊开了。Cell 还考虑了缓存行上的相互干扰,但这些实现细节需要按具体 JDK 和虚拟机观察。
sum 不是原子快照
sum() 会逐个读取分量。读取第一个分量之后,其他线程仍然可能继续修改后面的分量,因此它不保证获得某一个瞬间的完整快照。
访问量、事件次数这类统计通常能接受这个特点;余额扣减、限额判断则不能直接按同样方式处理。不能先 sum() 判断小于上限,再 increment(),就认为严格限制了并发总数。
reset() 和 sumThenReset() 在并发更新期间也不能当成精确切换统计窗口的原子操作。要求精确分界时,需要在外部协调写入。
LongAccumulator
LongAccumulator 把累加操作扩展成一个二元函数。例如求最大值:
import java.util.concurrent.atomic.LongAccumulator;
public class AccumulatorDemo {
public static void main(String[] args) {
LongAccumulator max = new LongAccumulator(Long::max, Long.MIN_VALUE);
max.accumulate(12);
max.accumulate(8);
max.accumulate(19);
System.out.println(max.get());
}
}结果是 19。函数应无副作用,并适合不确定的合并顺序;最大值、最小值、求和比较合适,依赖严格先后顺序的计算就不适合这样拆分。
LongAdder 的优势主要出现在竞争较大的统计场景,并且需要额外空间。没有测量任务的读写比例与竞争程度,不宜直接断言它总比 AtomicLong 快。