LongAdder和AtomicLong哪个性能更好,为什么?

概述

AtomicLong是作者Doug Leajdk1.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方法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l8HvYf1j-1597197228209)(evernotecid://A0276EB5-0616-4074-A4BE-0A1331D6E773/appyinxiangcom/1711267/ENResource/p9044)]

图:Unsafe类的getAndAddLong方法
可以清楚地看到,AtomicLong的原子性自增操作,是通过CAS实现的。
在多线程竞争不激烈的情况下,这样做是合适的。但是如果线程竞争激烈,会造成大量线程在原地打转、不停尝试去修改值,但是老是发现值被修改了,于是继续自旋。 这样浪费了大量的CPU资源
而且,由于AtomicLong持有的成员变量valuevolatile关键字修饰的,线程修改了临界资源后,需要刷新到其他线程,也是要费一番功夫的。
画个图来理解:
在这里插入图片描述

图:volatile刷新共享内存。
而LongAdder也有一个volatile修饰的base值,但是当竞争激烈时,多个线程并不会一直自旋来修改这个值,而是采用了分段的思想。竞争激烈时,各个线程会分散累加到自己所对应的Cell[]数组的某一个数组对象元素中,而不会大家共用一个。
这样做,可以把不同线程对应到不同的Cell中进行修改,降低了对临界资源的竞争。本质上,是用空间换时间。

LongAdder 和 AtomicLong 的性能对比

分析了半天,没有证据,还是不能让人信服的。唯有证明一下,才能心服口服。
接下来,我会创建一个容量为1,000的固定线程池,然后提交100倍于线程池容量的线程,每个线程中,对临界资源进行+1操作。等所有线程执行结束后,统计运行时长,并关闭线程池。 临界资源分别使用AtomicLongLongAdder表示,来对比二者的区别。
首先尝试让每个线程进行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次累加的运行结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ulRxhQVL-1597197228229)(evernotecid://A0276EB5-0616-4074-A4BE-0A1331D6E773/appyinxiangcom/1711267/ENResource/p9046)]

图:100次累加的执行结果
可以看到,AtomicLong耗时516毫秒,LongAdder耗时438毫秒
516➗438≈1.18,前者耗时是后者的一倍多一点。区别好像不是很大。

进行1,000次累加呢?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3Rz1RNxQ-1597197228232)(evernotecid://A0276EB5-0616-4074-A4BE-0A1331D6E773/appyinxiangcom/1711267/ENResource/p9047)]

图:1000次累加的执行结果
可以看到,AtomicLong耗时3034毫秒,LongAdder耗时575毫秒
3034➗575≈5.28,前者耗时是后者的5倍多。区别开始变得明显。

进行10,000次累加呢?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8JqWOcd7-1597197228234)(evernotecid://A0276EB5-0616-4074-A4BE-0A1331D6E773/appyinxiangcom/1711267/ENResource/p9048)]

图:10,000次累加的执行结果
可以看到,AtomicLong耗时30868毫秒,LongAdder耗时2167毫秒
30868➗2167≈14.24,前者耗时是后者的14倍多。差距变得更大了。

进行50,000次累加呢?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SOGZM5wg-1597197228235)(evernotecid://A0276EB5-0616-4074-A4BE-0A1331D6E773/appyinxiangcom/1711267/ENResource/p9049)]

图:50,000次累加的执行结果
可以看到,AtomicLong耗时148375毫秒,LongAdder耗时9754毫秒
148375➗9754≈15.21,前者耗时是后者的15倍多。差距进一步扩大。

结论

在每个线程执行的累加数量变多时,LongAdderAtomicLong性能优势越发明显。
LongAdder由于采用了分段理念,降低了线程间的竞争冲突,而AtomicLong却因多个线程并行竞争同一个value值,从而影响了性能。

在低竞争的情况下,AtomicLongLongAdder 这两个类具有相似的特征,吞吐量也是相似的,因为竞争不高。但是在竞争激烈的情况下,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
  • 7
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
AtomicLong是一个Java原子类,用于对长整型数据进行原子操作。它提供了一系列方法,可以实现原子性的增加、减少、设置、获取等操作。与AtomicInteger类似,AtomicLong的使用方式也类似。 AtomicLong是线程安全的,它使用了CAS(Compare and Swap)机制来保证操作的原子性。CAS是一种乐观锁的实现方式,它通过比较内存中的值与期望值来确定是否修改,如果相等则修改,否则重新尝试。这种方式可以避免使用锁,提高了并发性能。 在编译成字节码时,编译器会判断当前JVM或机器硬件是否支持8字节的CAS操作。如果支持,则使用无锁的方式实现原子操作;如果不支持,则会使用加锁处理。这样可以保证在不同的环境下都能正常使用AtomicLong。 总结来说,AtomicLong是一个用于对长整型数据进行原子操作的Java类,它提供了一系列方法,使用CAS机制来保证操作的原子性。它的使用方式与AtomicInteger类似,并且在编译器会根据环境选择合适的实现方式。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [Java concurrency之AtomicLong原子类_动力节点Java学院整理](https://download.csdn.net/download/weixin_38747818/12778861)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* *3* [并发编程 — 原子类 AtomicLong 详解](https://blog.csdn.net/small_love/article/details/111057686)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值