LongAdder和AtomicLong哪个性能更好,为什么?
概述
AtomicLong
是作者Doug Lea
在jdk1.5
版本发布于java.util.concurrent.atomic
并发包下的类。
而LongAdder
是道格·利(Doug Lea的中文名)
在java8
中发布的类。
有了AtomicLong为何还需要LongAdder?
在这里,就不得不分析一下AtomicLong
的缺点了。
先来看一下AtomicLong.incrementAndGet()
方法的源码
/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final long incrementAndGet() {
return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
}
接着跟踪Unsafe类的getAndAddLong
方法
图:Unsafe类的getAndAddLong
方法
可以清楚地看到,AtomicLong
的原子性自增操作,是通过CAS
实现的。
在多线程竞争不激烈的情况下,这样做是合适的。但是如果线程竞争激烈,会造成大量线程在原地打转、不停尝试去修改值,但是老是发现值被修改了,于是继续自旋。 这样浪费了大量的CPU资源
。
而且,由于AtomicLong
持有的成员变量value
是volatile
关键字修饰的,线程修改了临界资源后,需要刷新到其他线程,也是要费一番功夫的。
画个图来理解:
图:volatile
刷新共享内存。
而LongAdder也有一个volatile修饰的base
值,但是当竞争激烈时,多个线程并不会一直自旋来修改这个值,而是采用了分段的思想。竞争激烈时,各个线程会分散累加到自己所对应的Cell[]数组的某一个数组对象元素中,而不会大家共用一个。
这样做,可以把不同线程对应到不同的Cell中进行修改,降低了对临界资源的竞争。本质上,是用空间换时间。
LongAdder 和 AtomicLong 的性能对比
分析了半天,没有证据,还是不能让人信服的。唯有证明一下,才能心服口服。
接下来,我会创建一个容量为1,000
的固定线程池,然后提交100
倍于线程池容量的线程,每个线程中,对临界资源进行+1
操作。等所有线程执行结束后,统计运行时长,并关闭线程池。 临界资源分别使用AtomicLong
和LongAdder
表示,来对比二者的区别。
首先尝试让每个线程进行100次
+1操作,最后的累加结果应该为:线程数×100=1,000×100×100 = 10,000,000
AtomicLongDemo.java
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicLong;
/**
* <pre>
* 程序目的:演示 AtomicInteger、AtomicLong 在高并发下性能不好
* 在16个线程下使用AtomicLong。
* 每次值发生变化时,都会刷新回主内存,竞争激烈时,这样的 flush 和 refresh 操作耗费了很多资源,而且 CAS 也会经常失败
* </pre>
* created at 2020/8/11 06:11
* @author lerry
*/
public class AtomicLongDemo {
/**
* 线程池内线程数
*/
final static int POOL_SIZE = 1000;
public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();
AtomicLong counter = new AtomicLong(0);
ExecutorService service = Executors.newFixedThreadPool(POOL_SIZE);
ArrayList<Future> futures = new ArrayList<>(POOL_SIZE);
for (int i = 0; i < POOL_SIZE * 100; i++) {
futures.add(service.submit(new Task(counter)));
}
// 等待所有线程执行完
for (Future future : futures) {
try {
future.get();
}
catch (ExecutionException e) {
e.printStackTrace();
}
}
NumberFormat numberFormat = NumberFormat.getInstance();
System.out.printf("统计结果为:[%s]\n", numberFormat.format(counter.get()));
System.out.printf("耗时:[%d]毫秒", (System.currentTimeMillis() - start));
// 关闭线程池
service.shutdown();
}
/**
* 有一个 AtomicLong 成员变量,每次执行N次+1操作
*/
static class Task implements Runnable {
private final AtomicLong counter;
public Task(AtomicLong counter) {
this.counter = counter;
}
/**
* 每个线程执行N次+1操作
*/
@Override
public void run() {
for (int i = 0; i < 100; i++) {
counter.incrementAndGet();
}
}// end run
}// end class
}
LongAdderDemo.java
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.LongAdder;
/**
* <pre>
* 程序目的:和 AtomicLong 进行性能对比
* </pre>
* created at 2020/8/11 06:25
* @author lerry
*/
public class LongAdderDemo {
/**
* 线程池内线程数
*/
final static int POOL_SIZE = 1000;
public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();
LongAdder counter = new LongAdder();
ExecutorService service = Executors.newFixedThreadPool(POOL_SIZE);
ArrayList<Future> futures = new ArrayList<>(POOL_SIZE);
for (int i = 0; i < POOL_SIZE * 100; i++) {
futures.add(service.submit(new LongAdderDemo.Task(counter)));
}
// 等待所有线程执行完
for (Future future : futures) {
try {
future.get();
}
catch (ExecutionException e) {
e.printStackTrace();
}
}
NumberFormat numberFormat = NumberFormat.getInstance();
System.out.printf("统计结果为:[%s]\n", numberFormat.format(counter.sum()));
System.out.printf("耗时:[%d]毫秒", (System.currentTimeMillis() - start));
// 关闭线程池
service.shutdown();
}
/**
* 有一个 LongAdder 成员变量,每次执行N次+1操作
*/
static class Task implements Runnable {
private final LongAdder counter;
public Task(LongAdder counter) {
this.counter = counter;
}
/**
* 每个线程执行N次+1操作
*/
@Override
public void run() {
for (int i = 0; i < 100; i++) {
counter.increment();
}
}// end run
}// end class
}
备注:AtomicLong
的运行结果截图和LongAdder
的运行结果截图放在了一起,AtomicLong
的在上、LongAdder
的在下。
每个线程进行100
次累加的运行结果
图:100次累加的执行结果
可以看到,AtomicLong
耗时516毫秒
,LongAdder
耗时438毫秒
,516➗438≈1.18
,前者耗时是后者的一倍多一点。区别好像不是很大。
进行1,000
次累加呢?
图:1000
次累加的执行结果
可以看到,AtomicLong
耗时3034毫秒
,LongAdder
耗时575毫秒
,3034➗575≈5.28
,前者耗时是后者的5倍
多。区别开始变得明显。
进行10,000
次累加呢?
图:10,000
次累加的执行结果
可以看到,AtomicLong
耗时30868毫秒
,LongAdder
耗时2167毫秒
,30868➗2167≈14.24
,前者耗时是后者的14倍
多。差距变得更大了。
进行50,000
次累加呢?
图:50,000
次累加的执行结果
可以看到,AtomicLong
耗时148375毫秒
,LongAdder
耗时9754毫秒
,148375➗9754≈15.21
,前者耗时是后者的15倍
多。差距进一步扩大。
结论
在每个线程执行的累加数量变多时,LongAdder
比AtomicLong
性能优势越发明显。LongAdder
由于采用了分段理念
,降低了线程间的竞争冲突,而AtomicLong
却因多个线程并行竞争同一个value
值,从而影响了性能。
在低竞争的情况下,
AtomicLong
和LongAdder
这两个类具有相似的特征,吞吐量也是相似的,因为竞争不高。但是在竞争激烈的情况下,LongAdder
的预期吞吐量要高得多,经过试验,LongAdder
的吞吐量大约是AtomicLong
的十倍,不过凡事总要付出代价,LongAdder
在保证高效的同时,也需要消耗更多的空间。
参考资料
从 LongAdder 中窥见并发组件的设计思路 | 犀利豆的博客
环境说明
- java -version
java version "1.8.0_251"
Java(TM) SE Runtime Environment (build 1.8.0_251-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.251-b08, mixed mode)
- OS:
macOS High Sierra 10.13.4
上一篇: php 什么是可变变量?
推荐阅读
-
i5-7500和i3-8100哪个性能更好?i3-8100与i5-7500区别对比详细评测
-
为什么不建议选历生地?历生地和历政地哪个发展更好?
-
查问一下局部切换页面,使用iframe和ajax有没有性能上的差别,如果有,差距大吗?哪个性能更好
-
i5-7500和i3-8100哪个性能更好?i3-8100与i5-7500区别对比详细评测
-
查问一下局部切换页面,使用iframe和ajax有没有性能上的差别,如果有,差距大吗?哪个性能更好
-
盘查一下局部切换页面,使用iframe和ajax有没有性能上的差别,如果有,差距大吗?哪个性能更好
-
LongAdder和AtomicLong哪个性能更好,为什么?
-
为什么不建议选历生地?历生地和历政地哪个发展更好?