LongAdder(上)介绍篇

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

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

image-20200411011831604.png

上图为使用AtomicLong时,是多个线程同时竞争一个原子变量。**

image-20200411012357811.png

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

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

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

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

下节将具体使用以及代码分析!!

喜欢关注公众号:

qrcode

评论

You forgot to set the shortname for Disqus. Please set it in _config.yml.