Java多线程进阶(十七)—— J.U.C之atomic框架:LongAdder

2.jpg

本文首发于一世流云的专栏: https://segmentfault.com/blog...

1、LongAdder简介

JDK1.8时,java.util.concurrent.atomic包中提供了一个新的原子类:LongAdder
根据Oracle官方文档的介绍,LongAdder在高并发的场景下会比它的前辈————AtomicLong 具备更好的性能,代价是消耗更多的内存空间:
clipboard.pngjava

那么,问题来了:segmentfault

为何要引入LongAdderAtomicLong在高并发的场景下有什么问题吗? 若是低并发环境下,LongAdderAtomicLong性能差很少,那LongAdder是否就能够替代AtomicLong了?

为何要引入LongAdder?

咱们知道,AtomicLong是利用了底层的CAS操做来提供并发性的,好比addAndGet方法:数组

clipboard.png

上述方法调用了Unsafe类的getAndAddLong方法,该方法是个native方法,它的逻辑是采用自旋的方式不断更新目标值,直到更新成功。并发

在并发量较低的环境下,线程冲突的几率比较小,自旋的次数不会不少。可是,高并发环境下,N个线程同时进行自旋操做,会出现大量失败并不断自旋的状况,此时AtomicLong的自旋会成为瓶颈。dom

这就是LongAdder引入的初衷——解决高并发环境下AtomicLong的自旋瓶颈问题。函数

LongAdder快在哪里?

既然说到LongAdder能够显著提高高并发环境下的性能,那么它是如何作到的?这里先简单的说下LongAdder的思路,第二部分会详述LongAdder的原理。高并发

咱们知道,AtomicLong中有个内部变量value保存着实际的long值,全部的操做都是针对该变量进行。也就是说,高并发环境下,value变量实际上是一个热点,也就是N个线程竞争一个热点。性能

LongAdder的基本思路就是分散热点,将value值分散到一个数组中,不一样线程会命中到数组的不一样槽中,各个线程只对本身槽中的那个值进行CAS操做,这样热点就被分散了,冲突的几率就小不少。若是要获取真正的long值,只要将各个槽中的变量值累加返回。测试

这种作法有没有似曾相识的感受?没错,ConcurrentHashMap中的“分段锁”其实就是相似的思路。atom

LongAdder可否替代AtomicLong?

回答这个问题以前,咱们先来看下LongAdder提供的API:
clipboard.png

能够看到,LongAdder提供的API和AtomicLong比较接近,二者都能以原子的方式对long型变量进行增减。

可是AtomicLong提供的功能其实更丰富,尤为是addAndGetdecrementAndGetcompareAndSet这些方法。

addAndGetdecrementAndGet除了单纯的作自增自减外,还能够当即获取增减后的值,而LongAdder则须要作同步控制才能精确获取增减后的值。若是业务需求须要精确的控制计数,作计数比较,AtomicLong也更合适。

另外,从空间方面考虑,LongAdder实际上是一种“空间换时间”的思想,从这一点来说AtomicLong更适合。固然,若是你必定要跟我杠现代主机的内存对于这点消耗根本不算什么,那我也办法。

总之,低并发、通常的业务场景下AtomicLong是足够了。若是并发量不少,存在大量写多读少的状况,那LongAdder可能更合适。适合的才是最好的,若是真出现了须要考虑到底用AtomicLong好仍是LongAdder的业务场景,那么这样的讨论是没有意义的,由于这种状况下要么进行性能测试,以准确评估在当前业务场景下二者的性能,要么换个思路寻求其它解决方案。

最后,给出国外一位博主对LongAdder和AtomicLong的性能评测,以供参考:http://blog.palominolabs.com/...

2、LongAdder原理

以前说了,AtomicLong是多个线程针对单个热点值value进行原子操做。而LongAdder是每一个线程拥有本身的槽,各个线程通常只对本身槽中的那个值进行CAS操做。

好比有三个ThreadA、ThreadB、ThreadC,每一个线程对value增长10。

对于AtomicLong,最终结果的计算始终是下面这个形式:
$$ value = 10 + 10 + 10 = 30 $$

可是对于LongAdder来讲,内部有一个base变量,一个Cell[]数组。
base变量:非竞态条件下,直接累加到该变量上
Cell[]数组:竞态条件下,累加个各个线程本身的槽Cell[i]
最终结果的计算是下面这个形式:
$$ value = base + \sum_{i=0}^nCell[i] $$

LongAdder的内部结构

LongAdder只有一个空构造器,其自己也没有什么特殊的地方,全部复杂的逻辑都在它的父类Striped64中。
clipboard.png

来看下Striped64的内部结构,这个类实现一些核心操做,处理64位数据。
Striped64只有一个空构造器,初始化时,经过Unsafe获取到类字段的偏移量,以便后续CAS操做:
clipboard.png

上面有个比较特殊的字段是threadLocalRandomProbe,能够把它当作是线程的hash值。这个后面咱们会讲到。

定义了一个内部Cell类,这就是咱们以前所说的槽,每一个Cell对象存有一个value值,能够经过Unsafe来CAS操做它的值:
clipboard.png

其它的字段:
能够看到Cell[]就是以前提到的槽数组,base就是非并发条件下的基数累计值。
clipboard.png

LongAdder的核心方法

仍是经过例子来看:
假设如今有一个LongAdder对象la,四个线程A、B、C、D同时对la进行累加操做。

LongAdder la = new LongAdder();
la.add(10);

ThreadA调用add方法(假设此时没有并发):
clipboard.png

初始时Cell[]为null,base为0。因此ThreadA会调用casBase方法(定义在Striped64中),由于没有并发,CAS操做成功将base变为10:
clipboard.png

能够看到,若是线程A、B、C、D线性执行,那casBase永远不会失败,也就永远不会进入到base方法的if块中,全部的值都会累积到base中。
那么,若是任意线程有并发冲突,致使caseBase失败呢?

失败就会进入if方法体:
clipboard.png

这个方法体会先再次判断Cell[]槽数组有没初始化过,若是初始化过了,之后全部的CAS操做都只针对槽中的Cell;不然,进入longAccumulate方法。

整个add方法的逻辑以下图:
clipboard.png

能够看到,只有从未出现过并发冲突的时候,base基数才会使用到,一旦出现了并发冲突,以后全部的操做都只针对 Cell[]数组中的单元Cell。
若是 Cell[]数组未初始化,会调用父类的 longAccumelate去初始化 Cell[],若是 Cell[]已经初始化可是冲突发生在 Cell单元内,则也调用父类的 longAccumelate,此时可能就须要对 Cell[]扩容了。

这也是LongAdder设计的精妙之处:尽可能减小热点冲突,不到最后万不得已,尽可能将CAS操做延迟。

Striped64的核心方法

咱们来看下Striped64的核心方法longAccumulate到底作了什么:
clipboard.png

上述代码首先给当前线程分配一个hash值,而后进入一个自旋,这个自旋分为三个分支:

  • CASE1:Cell[]数组已经初始化
  • CASE2:Cell[]数组未初始化
  • CASE3:Cell[]数组正在初始化中

CASE2:Cell[]数组未初始化

咱们以前讨论了,初始时Cell[]数组尚未初始化,因此会进入分支②:
clipboard.png

首先会将cellsBusy置为1-加锁状态
clipboard.png

而后,初始化Cell[]数组(初始大小为2),根据当前线程的hash值计算映射的索引,并建立对应的Cell对象,Cell单元中的初始值x就是本次要累加的值。

CASE3:Cell[]数组正在初始化中

若是在初始化过程当中,另外一个线程ThreadB也进入了longAccumulate方法,就会进入分支③:
clipboard.png

能够看到,分支③直接操做base基数,将值累加到base上。

CASE1:Cell[]数组已经初始化

若是初始化完成后,其它线程也进入了longAccumulate方法,就会进入分支①:
clipboard.png

整个longAccumulate的流程图以下:
clipboard.png

LongAdder的sum方法

最后,咱们来看下LongAddersum方法:
clipboard.png

sum求和的公式就是咱们开头说的:
$$ value = base + \sum_{i=0}^nCell[i] $$

须要注意的是,这个方法只能获得某个时刻的近似值,这也就是LongAdder并不能彻底替代LongAtomic的缘由之一。

3、LongAdder的其它兄弟

JDK1.8时,java.util.concurrent.atomic包中,除了新引入LongAdder外,还有引入了它的三个兄弟类:LongAccumulatorDoubleAdderDoubleAccumulator

clipboard.png

LongAccumulator

LongAccumulatorLongAdder的加强版。LongAdder只能针对数值的进行加减运算,而LongAccumulator提供了自定义的函数操做。其构造函数以下:
clipboard.png

经过LongBinaryOperator,能够自定义对入参的任意操做,并返回结果(LongBinaryOperator接收2个long做为参数,并返回1个long)

LongAccumulator内部原理和LongAdder几乎彻底同样,都是利用了父类Striped64longAccumulate方法。这里就再也不赘述了,读者能够本身阅读源码。

DoubleAdder和DoubleAccumulator

从名字也能够看出,DoubleAdderDoubleAccumulator用于操做double原始类型。

LongAdder的惟一区别就是,其内部会经过一些方法,将原始的double类型,转换为long类型,其他和LongAdder彻底同样:
clipboard.png

相关文章
相关标签/搜索