本文主要是介绍多线程篇(并发相关类- 原子操作类)(持续更新迭代),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
目录
前言
一、原子变量操作类(AtomicLong为例)
1. 前言
2. 实例
二、JDK 8新增的原子操作类LongAdder
三、LongAccumulator类原理探究
前言
JUC包提供了一系列的原子性操作类,这些类都是使用非阻塞算法CAS实现的,相比使用锁实现原子性操作这在性能上有很大提高。
由于原子性操作类的原理都大致相同,这里讲解最简单的AtomicLong类的实现原理以及JDK 8中新增的LongAdder和LongAccumulator
类的原理。有了这些基础,再去理解其他原子性操作类的实现就不会感到困难了。
一、原子变量操作类(AtomicLong为例)
1. 前言
JUC 并发包中包含有 AtomicInteger、AtomicLong 和 AtomicBoolean 等原子性操作类它们的原理类似,我们主要讲解 AtomicLong
类。
AtomicLong 是原子性递增或者递减类,其内部使用 Unsafe 来实现。
通过Unsafe.getUnsafe()方法获取到 Unsafe 类的实例,这里你可能会有疑问,为何能通过 Unsafe.getUnsafe()方法获取到 Unsafe 类的
实例?
其实这是因为 AtomicLong类也是在 rt.jar 包下面的,AtomicLong 类就是通过 BootStarp 类加载器进行加载的。
在没有原子类的情况下,实现计数器需要使用一定的同步措施,比如使用 synchronized 关键字等,但是这些都是阻塞算法,对性能有
一定损耗,而原子操作类都使用 CAS 非阻塞算法,性能更好。
但是在高并发情况下AtomicLong 还会存在性能问题。
JDK 8 提供了一个在高并发下性能更好的 LongAdder 类。
2. 实例
AtomicLong是原子性递增或者递减类,其内部使用Unsafe来实现,我们看下面的代码:
- 代码(1)通过Unsafe.getUnsafe()方法获取到Unsafe类的实例,这里你可能会有疑问,为何能通过Unsafe.getUnsafe()方法获取到Unsafe类的实例?其实这是因为AtomicLong类也是在rt.jar包下面的,AtomicLong类就是通过BootStarp类加载器进行加载的。
- 代码(5)中的value被声明为volatile的,这是为了在多线程下保证内存可见性,value是具体存放计数的变量。
- 代码(2)(4)获取value变量在AtomicLong类中的偏移量。
下面重点看下AtomicLong中的主要函数:
递增和递减操作代码:
在如上代码内部都是通过调用Unsafe的getAndAddLong方法来实现操作,这个函数是个原子性操作,这里第一个参数是AtomicLong实
例的引用,第二个参数是value变量在AtomicLong中的偏移值,第三个参数是要设置的第二个变量的值。
boolean compareAndSet(long expect, long update)方法:
在内部调用了unsafe.compareAndSwapLong方法。
如果原子变量中的value值等于expect,则使用update值更新该值并返回true,否则返回false。
示例代码:多线程使用AtomicLong统计0的个数。
public class Atomic {// (10) 创建Long型原子计数器private static final AtomicLong atomicLong = new AtomicLong();// (11) 创建数据源private static final Integer[] arrayOne = new Integer[]{0, 1, 2, 3, 0, 5, 6, 0, 56, 0};private static final Integer[] arrayTwo = new Integer[]{10, 1, 2, 3, 0, 5, 6, 0, 56, 0};public static void main(String[] args) throws InterruptedException {//(12)线程one统计数组arrayOne中0的个数Thread threadOne = new Thread(() -> {int size = arrayOne.length;for (Integer integer : arrayOne) {if (integer == 0) {atomicLong.incrementAndGet();}}});//(13)线程two统计数组arrayTwo中0的个数Thread threadTwo = new Thread(() -> {int size = arrayTwo.length;for (Integer integer : arrayTwo) {if (integer == 0) {atomicLong.incrementAndGet();}}});// (14) 启动子线程threadOne.start();threadTwo.start();// (15) 等待线程执行完毕threadOne.join();threadTwo.join();System.out.println("arrayOne & arrayTwo 0 的个数是: " + atomicLong.get());}
}
运行结果:
如上代码中的两个线程各自统计自己所持数据中0的个数,每当找到一个0就会调用AtomicLong的原子性递增方法。
在没有原子类的情况下,实现计数器需要使用一定的同步措施,比如使用synchronized关键字等,但是这些都是阻塞算法,对性能有一定
损耗,而本章介绍的这些原子操作类都使用CAS非阻塞算法,性能更好。
但是在高并发情况下AtomicLong还会存在性能问题。
JDK 8提供了一个在高并发下性能更好的LongAdder类,下面我们来讲解这个类。
二、JDK 8新增的原子操作类LongAdder
AtomicLong通过CAS提供了非阻塞的原子性操作,相比使用阻塞算法的同步器来说它的性能已经很好了,但是JDK开发组并不满足于此。
使用AtomicLong时,在高并发下大量线程会同时去竞争更新同一个原子变量,但是由于同时只有一个线程的CAS操作会成功,这就造成
了大量线程竞争失败后,会通过无限循环不断进行自旋尝试CAS的操作,而这会白白浪费CPU资源。
因此JDK 8新增了一个原子性递增或者递减类LongAdder用来克服在高并发下使用AtomicLong的缺点。既然AtomicLong的性能瓶颈是由
于过多线程同时去竞争一个变量的更新而产生的,那么如果把一个变量分解为多个变量,让同样多的线程去竞争多个资源,是不是就解决
了性能问题?是的,LongAdder就是这个思路。下面通过图来理解两者设计的不同之处,如图所示。
使用LongAdder时,则是在内部维护多个Cell变量,每个Cell里面有一个初始值为0的long型变量,这样,在同等并发量的情况下,争夺单
个变量更新操作的线程量会减少,这变相地减少了争夺共享资源的并发量。另外,多个线程在争夺同一个Cell原子变量时如果失败了,它
并不是在当前Cell变量上一直自旋CAS重试,而是尝试在其他Cell的变量上进行CAS尝试,这个改变增加了当前线程重试CAS成功的可能
性。最后,在获取LongAdder当前值时,是把所有Cell变量的value值累加后再加上base返回的。
该类通过内部cells数组分担了高并发下多线程同时对一个原子变量进行更新时的竞争量,让多个线程可以同时对cells数组里面的元素进行
并行的更新操作。
另外,数组元素Cell使用@sun.misc.Contended注解进行修饰,这避免了cells数组内多个原子变量被放入同一个缓存行,也就是避免了
伪共享,这对性能也是一个提升。
三、LongAccumulator类原理探究
LongAdder类是LongAccumulator的一个特例,LongAccumulator比LongAdder的功能更强大。
例如下面的构造函数,其中accumulatorFunction是一个双目运算器接口,其根据输入的两个参数返回一个计算值,
identity则是LongAccumulator累加器的初始值。
上面提到,LongAdder其实是LongAccumulator的一个特例,调用LongAdder就相当于使用下面的方式调用LongAccumulator:
LongAccumulator相比于LongAdder,可以为累加器提供非0的初始值,后者只能提供默认的0值。另外,前者还可以指定累加规则,
比如不进行累加而进行相乘,只需要在构造LongAccumulator时传入自定义的双目运算器即可,后者则内置累加的规则。
这篇关于多线程篇(并发相关类- 原子操作类)(持续更新迭代)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!