阿里为什么推荐使用LongAdder?而不是AtomicLong?

Posted xhmj12

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了阿里为什么推荐使用LongAdder?而不是AtomicLong?相关的知识,希望对你有一定的参考价值。

相关阅读:一款神仙接私活低代码平台,吊到不行(附源码)

原文链接:https://blog.csdn.net/limenghua9112/article/details/107950744

概述

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 在保证高效的同时,也需要消耗更多的空间。

参考资料

https://xilidou.com/2018/11/27/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


 


 

 

 

 

 

 

 

 

 

 

 
-End-

1、985副教授工资曝光

2、心态崩了!税前2万4,到手1万4,年终奖扣税方式1月1日起施行~

3、雷军做程序员时写的博客,很强大!

4、人脸识别的时候,一定要穿上衣服啊!

5、清华大学:2021 元宇宙研究报告!

6、绩效被打3.25B,员工将支付宝告上了法院,判了

以上是关于阿里为什么推荐使用LongAdder?而不是AtomicLong?的主要内容,如果未能解决你的问题,请参考以下文章

AtomicXXX 用的好好的,阿里为什么推荐使用 LongAdder?面试必问!

javaLongAdder源码分析原理分析

美团五大最受欢迎的开源项目!

Git 不要只会 pull 和 push,试试这 5 条提高效率的命令

面试官:为什么LongAdder性能比long还快

瞧瞧,人家这后端API接口写得,那叫一个巴适~,再看看我的,像坨屎!