JAVA并发编程高级——JDK 新增的原子操作类 LongAdder

LongAdder 简单介绍

        前面讲过,AtomicLong通过CAS提供了非阻塞的原子性操作,相比使用阻塞算法的同步器来说它的性能已经很好了,但是JDK开发组并不满足于此。使用AtomicLong 时,在高并发下大量线程会同时去竞争更新同一个原子变量,但是由于同时只有一个线程的CAS操作会成功,这就造成了大量线程竞争失败后,会通过无限循环不断进行自旋尝试CAS 的操作,而这会白白浪费 CPU 资源。

        因此JDK8新增了一个原子性递增或者递减类LongAdder用来克服在高并发下使用AtomicLong的缺点。既然 AtomicLong的性能瓶颈是由于过多线程同时去竞争一个变量的更新而产生的,那么如果把一个变量分解为多个变量,让同样多的线程去竞争多个资源,是不是就解决了性能问题?是的,LongAdder 就是这个思路。下面通过图来理解两者设计的不同之处,如图 4-1 所示。

如图 4-1所示,使用 AtomicLong时,是多个线程同时竞争同一个原子变量。

        如图 4-2所示,使用LongAdder时,则是在内部维护多个Cell变量,每个Cell 里面有一个初始值为0的long型变量,这样,在同等并发量的情况下,争夺单个变量更新操作的线程量会减少,这变相地减少了争夺共享资源的并发量。另外,多个线程在争夺同一个Cel 原子变量时如果失败了,它并不是在当前 Cel 变量上一直自旋 CAS 重试,而是尝试在其他 Cell的变量上进行 CAS尝试,这个改变增加了当前线程重试 CAS成功的可能性。最后,在获取LongAdder 当前值时,是把所有 Cell 变量的 value 值累加后再加上 base 返回的。        

        LongAdder 维护了一个延迟初始化的原子性更新数组(默认情况下Cell 数组是 null)和一个基值变量base。由于Cells 占用的内存是相对比较大的,所以一开始并不创建它而是在需要时创建,也就是惰性加载。

        当一开始判断 Cell 数组是nul 并且并发线程较少时,所有的累加操作都是对 base 变量进行的。保持Cell数组的大小为2的N次方,在初始化时Cell数组中的Cell 元素个数为2,数组里面的变量实体是Cell 类型。Cell类型是AtomicLong的一个改进,用来减少缓存的争用,也就是解决伪共享问题。

        对于大多数孤立的多个原子操作进行字节填充是浪费的,因为原子性操作都是无规律地分散在内存中的(也就是说多个原子性变量的内存地址是不连续的),多个原子变量被放入同一个缓存行的可能性很小。但是原子性数组元素的内存地址是连续的,所以数组内的多个元素能经常共享缓存行,因此这里使用@sun.misc.Contended 注解对 Cell 类进行字节填充,这防止了数组中多个元素共享一个缓存行,在性能上是一个提升。

LongAdder 代码分析     

        为了解决高并发下多线程对一个变量CAS争夺失败后进行自旋而造成的降低并发性能问题,LongAdder在内部维护多个Cell元素(一个动态的Cell数组)来分担对单个变量进行争夺的开销。下面围绕以下话题从源码角度来分析LongAdder的实现:

(1)LongAdder 的结构是怎样的?

(2)当前线程应该访问 Cell 数组里面的哪一个Cell元素?

(3)如何初始化Cell数组?

(4)Cell 数组如何扩容?

(5)线程访问分配的Cell 元素有冲突后如何处理?

(6)如何保证线程操作被分配的Cell元素的原子性?   

        首先看下 LongAdder 的类图结构,如图 4-3 所示

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值