LongAdder是jdk 1.8引入的一个类,宣称比AtomiLong更高效。它内部有一个基本数base和一个cell数组,在高并发的情况下各个线程将值存放在了数组中,在低并发的情况下直接在一个base数上做计算,取值的时候把基本数和cell数组中的值做累加返回。LongAdder采用了类似分段锁的设计,降低了竞争。
其实看到取值的时候我是懵的,取值的代码如下:
public long sum() {
Cell[] as = cells; Cell a;
long sum = base;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
注释里说了,当没有并发更新的时候,这个值是准确的。当有并发更新的时候,这个值就不准确了。原因很简单,虽然base和cells都是volatile类型,保证了可见性,然而我累加到一半的时候,base因为并发更新而改变了,可我已经累加过了,拿到的结果肯定没有包括这期间新增的值。
令人沮丧的是,在并发期间,这个真实的值几乎是不可用的。虽然LongAdder存起来了各个确定的值,但是我却拿不到这个准确的和。
因此LongAdder,应该只适用于对计数不要求那么精确(虽然它是精确的保存了所有值的),或者只关心最终的值(并发已经结束,可以拿到最终的结果)。在阿里巴巴JAVA开发手册中,稍微提到了LongAdder在做类似count++操作方面的优势,如果只是想统计下最后的个数,那么LongAdder肯定是最好的选择。
下面附上LongAdder和AtomicLong在做count++上面的比较,测试代码如下:
@Data
@Slf4j
public class LongAdderTest{
public static final int COR = 1000;
public static final int MAX = 10000;
public static final int THREAD_COUNT = 1000; // 通过修改并发线程数和循环次数来测试
public static final int LOOP = 1000000; // 通过修改并发线程数和循环次数来测试
public static void main(String[] args) throws Exception{
ExecutorService pool = new ThreadPoolExecutor(COR,MAX,60L, TimeUnit.SECONDS,new LinkedBlockingQueue<>());
final LongAdder adder = new LongAdder();
final AtomicLong count = new AtomicLong();
CompletionService completionService = new ExecutorCompletionService<>(pool);
long start = System.currentTimeMillis();
for(int i =0;i<THREAD_COUNT;i++){
completionService.submit(() -> {
for (int j = 0; j < LOOP; j++) {
//count.incrementAndGet();
adder.increment();
}
return 1L;
});
}
for (int i = 0; i < THREAD_COUNT; i++) {
Future<Long> future = completionService.take();
future.get();
}
System.out.println("耗时:" + (System.currentTimeMillis() - start) + "ms");
pool.shutdown();
}
我们固定循环100W次,然后改变并发线程数,下面是测试结果:
10 | 100 | 1000 | |
---|---|---|---|
AtomicLong | 441ms | 2.931s | 28.851s |
LongAdder | 203ms | 706ms | 6.526s |
可以看到,在低并发的时候两者区别并不是很大,在高并发的时候两者耗时就不是一个数量级了。