Java LongAdder原子加法器源码深度解析

基于JDK1.8详细介绍了JUC下面的LongAdder原子类源码和原理,LongAdder是Java8对于原子类的增强。

文章目录

  • 1 原子类的加强
    • 1.1 LongAdder
      • 1.1.1 LongAdder的概述
      • 1.1.2 LongAdder的原理
        • 1.1.2.1 内部结构
        • 1.1.2.2 add增加给定值
          • 1.1.2.2.1 longAccumulate统一处理
        • 1.1.2.3 increment自增
        • 1.1.2.4 sum统计
        • 1.1.2.5 reset重置
        • 1.1.2.6 sumThenReset统计并重置
  • 2 JMH性能测试
  • 3 atomic的总结

1 原子类的加强

JDK1.8的时候,新增了四个原子类:

  1. LongAdder:long类型的数值累加器,从0开始累加,累加规则为加法运算。
  2. LongAccumulator:long类型的数值累加器,可从指定值开始累加,可指定累加规则。
  3. DoubleAdder:double类型的数值累加器,从0开始累加,累加规则为加法运算。
  4. DoubleAccumulator:double类型的数值累加器,可从指定值开始累加,可指定累加规则。

自从原子类问世之后,多线程环境下如果用于统计计数操作,一般可以使用AtomicLong来代替锁作为计数器,AtomicLong 通过CAS 提供了非阻塞的原子性操作,相比使用阻塞算法的同步器来说它的性能己经很好了,那么,它们有什么缺点吗?

实际上,AtomicLong等其他传统的atomic原子类对于数值的更改,通常都是在一个无限循环(自旋)中不断尝试CAS 的修改操作,一旦CAS失败则循环重试,这样来保证最终CAS操作成功。如果竞争不激烈,那么修改成功的概率就很高,但是如果在高并发下大量线程频繁的竞争修改计数器,会造成一次CAS修改失败的概率就很高。在大量修改失败时,这些原子操作就会进行多次循环尝试,白白浪费CPU 资源,因此性能还是会受到影响。

JDK1.8新增这些类,正是为了解决高并发环境下由于频繁读写AtomicLong等计数器而可能造成某些线程持续的空转(循环)进而浪费CPU的情况,它们也被称为“累加器”!

LongAdder和DoubleAdder,LongAccumulator和DoubleAccumulator的原理差不多。实际上DoubleAdder中对于double的累加也是先通过Double.doubleToRawLongBits将double类型转换为long类型来进行计算的,并且底层也是存储的long类型的值,在获取总和的时候又会通过Double.longBitsToDouble将存储的long值转换为double。

下面我们将对LongAdder进行讲解!

1.1 LongAdder

1.1.1 LongAdder的概述

public class LongAdder
extends Number
implements Serializable

来自于JDK 1.8的LongAdder,作为一个long类型数值的累加器,被用来克服在高并发下使用AtomicLong 可能由于线程频繁自旋而浪费CPU的缺点。

LongAdder的解决方式是采用了“热点数据分离”的基本思想:
传统的原子类的内部通常维护了一个对应类型的value属性值,多个线程之间的CAS竞争实际上就是在争夺对这个value属性的更新权,但是CAS操作只会保证同时只有一个线程能够更新成功,因此AtomicLong(包括其他传统原子类)的性能瓶颈就是由于过多线程同时去竞争一个变量的更新而产生的,那么如果把一个变量分解为多个变量,让同样多的线程去竞争多个资源,最终的结果就是统计被分解出来的多个变量的总和,这样就能大大缓解多线程竞争导致的性能问题,这就是“热点数据分离”的基本思想。这种思想在高并发环境下非常有用,类似的还有“减小锁的粒度”的思想,除了新的原子类之外,在JDK1.8的ConcurrentHashMap中对于结点数量的统计并没有采用单个变量计数,也是采用的类似于LongAdder的“热点数据分离”的基本思想。

在这里插入图片描述

1.1.2 LongAdder的原理

1.1.2.1 内部结构

下面是Striped64中的常用属性,LongAdder实际上就是使用的这些属性, 没什么自己的特别的属性:

//Striped64中的属性

/**
 * 用来实现CAS锁的资源,值为0时表示没有锁,值为1时表示已上锁,扩容Cell 数组或者初始化Cell 数组时会使用到该值
 * 使用CAS的同时唯一成功性来保证同一时刻只有一条线程可以进入扩容Cell 数组或者初始化Cell 数组的代码
 */
transient volatile int cellsBusy;

/**
 * volatile long 类型的基本属性,在没有CAS竞争时用来统计计数
 */
transient volatile long base;

/**
 * volatile Cell类型的数组,要么为null,当发生CAS更新base出现竞争的时候初始化
 * 此后就一直使用该数组来统计计数,初始容量为2,数组可扩容,大小为2的幂次方
 */
transient volatile Striped64.Cell[] cells;

/**
 1. ccells数组的元素类型,由于是数组,导致内存连续,因此可以使用缓存填充(注解方式)来避免伪共享。
 */
@sun.misc.Contended
static final class Cell {
    /**
     * 内部就是一个volatile long类型的基本属性,线程对数组某个索引位置的更新实际上就是更新该值
     */
    volatile long value;

    Cell(long x) {
        value = x;
    }

    /**
     * 更新value指的CAS方法
     *
     * @param cmp 预期值
     * @param val 新值
     * @return true 成功  false 失败
     */
    final boolean cas(long cmp, long val) {
        return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
    }

    //对于数值的更新都是CAS的操作

    private static final sun.misc.Unsafe UNSAFE;
    private static final long valueOffset;

    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> ak = Striped64.Cell.class;
            valueOffset = UNSAFE.objectFieldOffset
                    (ak.getDeclaredField("value"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }
}

LongAdder类继承自Striped64类,Striped64被设计用来减少高并发环境的线程冲突,Striped64类是对外不可见的,也是这四个累加器类的公共抽象父类,它们很多的操作也是直接调用Striped64的方法,在Striped64 内部维护着三个主要的变量:

  1. cellsBusy :用来实现简单的CAS锁,状态值只有0和l,当创建Cell 元素,扩容Cell 数组或者初始化Cell 数组时,使用CAS 操作该变量来保证同时只有一个线程可以进行其中之一的操作。
  2. base:volatile int类型的一个基本属性,热点分离的实现之一,在没有存在并发CAS操作的时候记录被用于记录累加值,也用来记录初始值。
  3. cells:volatile Cell[ ] 类型的一个对象数组,热点分离的实现之二,当使用CAS更新base基值失败(出现CAS竞争)的时候,就会初始化该数组,然后尝试通过更新该数组中的某个位置的值来记录累加值。

由此我们可以明确的知道,LongAdder的热点分离思想的具体实现是将value分散为一个base变量+一个cells数组。

这里采用数组的用途很明显,那就是对于并发下的线程随机分配到数组不同索引位置,并对该位置的值进行更新,因此理论上采用一个数组就行了,那么为什么不采用单独一个数组还要加一个变量呢?在没有竞争的情况下,如果还是初始化一个数组然后更新数组某个索引的值就有些得不偿失了,因为数组明显比单个变量占用更多的空间,其更新效率也没有单独更新一个变量那么块。

因此,综合考虑下LongAdder采用一个变量base和一个数组cells一起来计数,它们的使用流程如下:在更新计数的时候如果没有CAS竞争,即并发度较低时就一直使用base变量来统计计数,此时cells数组是null,即没有初始化或者锁延迟初始化,就和AtomicLong一样。一旦出现对base变量的CAS竞争,即高并发环境下某些线程CAS更新base失败,那么就初始化cells数组,并且此后都使用cells数组来进行统计计数,如果数组某一个索引位置的Cell更新时仍然出现了竞争,那么cells数组可能会扩容或者寻找新的Cell。在统计总和时对base和cells数组中的值进行求和即可,这种方法在热点分离的基础上还优化了内存的开销。

初始化cells数组中的容量为2,扩容时必须保证容量为2的幂次方,数组里面的数据是Cell 类型,Cell类中仅仅只有一个value属性,实际上就是对value值的封装,封装成为类的原因主要是方便调用方法对某个位置的value值进行CAS的更新,以及作缓存填充操作。

因为数组的内存空间必须是连续的,而一个cell内部只有一个int value属性,非常有可能多个cell对象存在同一个缓存行中,当CAS的更新某一个Cell的值时会将该Cell所属的缓存行失效,因此会同时造成其他位于同一个缓存行的相邻Cell缓存也同时失效,这样后续线程必须重主存获取相邻的Cell,这就造成了“伪共享”的问题,两个Cell的访问应该是互不影响的,但是由于在同一个缓存行,造成了和“共享”的现象,因此称为“伪共享”。这里的Cell 类使用了 @sun.misc.Contended注解修饰,这是JDK1.8缓存填充的新方式,这样一个Cell对象就占据一个缓存行的大小,解决了伪共享的问题,进一步提升了性能。 关于伪共享,可以看这篇文章:Java中的伪共享深度解析以及避免方法。

LongAdder仅有一个空构造器:

/**
 * 空的构造器,Cell数组延迟初始化
 */
public LongAdder() {
}
1.1.2.2 add增加给定值

public void add(long x)

add方法用于增加给定值。这个方法是LongAdder的核心方法之一。代码比较多,且比较难以理解。

大概步骤为:

  1. as变量保存此时的cells数组。判断当as为null时,那么CAS更新base的值,如果更新成功,那么add方法就结束了,这就是采用base属性更新的逻辑。
  2. 如果as不为null,或者CAS更新base失败之后,都会进入if代码块,内部就是采用cells数组更新的逻辑:
    1. uncontended变量表示冲突的标记,初始化true。
    2. if代码块中又是一个if判断,通过||连接四个表达式:
      1. 如果as为null,说明cells数组没有初始化。如果条件满足,继续那么进入if代码块,如果该条件不满足,继续向后判断;
      2. m等于cells数组长度减一。如果m小于0,说明数组没初始化完毕。如果条件满足,继续那么进入if代码块,如果该条件不满足,继续向后判断。
      3. getProbe()用于获取当前线程的threadLocalRandomProbe值,是一个随机生成的探测哈希值,不同的线程不一样,初始值为0。通过getProbe() & m 计算当前线程应该访问数组的某个索引元素并赋值给a。另外,threadLocalRandomProbe也被用在ThreadLocalRandom中。如果a为null,说明该索引位置还没有初始化元素对象。如果条件满足,继续那么进入if代码块,如果该条件不满足,继续向后判断。
      4. 最后调用a.cas方法尝试CAS的将base值从v = a.value更新为v+x,即,使用该位置记录增加值,CAS的结果赋值给uncontended,如果还是CAS更新失败,即说明这个位置还是有冲突。如果条件满足,继续那么进入if代码块,如果该条件不满足,表示CAS成功,使用数组该位置的CAS记录更新成功,那么add方法结束。
      5. 即如果上面的四个表达式有一个返回true,那么就是进入if代码块,表示cells需要(正在)初始化、或者某个位置的Cell需要初始化,或者cells的竞争激烈需要扩容。在if代码块中调用longAccumulate方法,该方法是Striped64的方法,用于进一步处理上面的问题并且最终会新增给定值成功,add方法结束。传递参数为:x、null、uncontended。实际上即使进入了longAccumulate方法,还是有可能最终会使用base属性进行更新的,那就是多个线程判断cells为null并同时进入的情况下,后面会讲到。
/**
 * 增加给定值
 *
 * @param x 给定值
 */
public void add(long x) {
    //初始化一些变量
    Striped64.Cell[] as;
    long b, v;
    int m;
    Striped64.Cell a;
    /*
     * as保存此时的cells数组:
     * 如果不为null,那么直接进入if代码块
     * 如果为null,说明合格数组还没有初始化,执行后面的casBase操作,b保存base的值
     * 随后尝试CAS的将base值从b更新为b + x,即使用base记录增加值,如果CAS成功那么不进入if代码块,add方法就结束了
     * CAS失败同样进入if代码块,表示CAS更新bae的值出现了并发竞争
     *
     * 即,当cells数组为null并且CAS更新base的值成功之后,add方法就结束了,这就是采用base更新的逻辑
     * 如果cells不为null,或者CAS更新base失败之后,都会进入if代码块,下面就是采用数组更新的逻辑
     */
    if ((as = cells) != null || !casBase(b = base, b + x)) {
        //uncontended用于表示是否没有进行CAS操作,初始化true,当CAS失败的时候会变成false
        boolean uncontended = true;
        /*
         * as == null
         *      如果as为null,说明cells数组没有初始化。如果条件满足,继续那么进入if代码块,如果该条件不满足,继续向后判断
         * (m = as.length - 1) < 0
         *      m等于cells数组长度减一,如果m小于0,说明数组没初始化完毕。如果条件满足,继续那么进入if代码块,如果该条件不满足,继续向后判断
         * (a = as[getProbe() & m]) == null
         *      getProbe()用于获取当前线程的threadLocalRandomProbe值,是一个随机生成的探测哈希值,不同的线程不一样,初始值为0
         *      通过getProbe() & m 计算当前线程应该访问数组的某个索引元素并赋值给a。另外,threadLocalRandomProbe也被用在ThreadLocalRandom中
         *      如果a为null,说明该索引位置还没有初始化元素对象。如果条件满足,继续那么进入if代码块,如果该条件不满足,继续向后判断
         * !(uncontended = a.cas(v = a.value, v + x))
         *      最后调用a.cas方法尝试CAS的将base值从v = a.value更新为v+x,即使用该位置记录增加值,CAS的结果赋值给uncontended,如果还是CAS更新失败,即说明这个位置还是有冲突。
         *      如果条件满足,继续那么进入if代码块,如果该条件不满足,表示CAS成功,使用数组该位置的CAS记录更新成功,那么add方法结束
         *
         * 即如果上面的四个表达式有一个返回true,那么就是进入if代码块,表示cells需要/正在初始化、或者某个位置的Cell需要初始化,或者cells的竞争激烈需要扩容
         */
        if (as == null || (m = as.length - 1) < 0 ||
                (a = as[getProbe() & m]) == null ||
                !(uncontended = a.cas(v = a.value, v + x)))
            //调用longAccumulate方法进一步处理,该方法是Striped64的方法,传递x、null、uncontended
            longAccumulate(x, null, );
    }
}

/**
 * 尝试CAS的将base值从cmp更新为val
 *
 * @param cmp 预期base值
 * @param val 新base值
 * @return 成功返回true;失败返回false
 */
final boolean casBase(long cmp, long val) {
    return UNSAFE.compareAndSwapLong(this, BASE, cmp, val);
}

/**
 1. 获取当前线程的探测哈希值,不同的线程不一样,初始值为0
 */
static final int getProbe() {
    return UNSAFE.getInt(Thread.currentThread(), PROBE);
}
1.1.2.2.1 longAccumulate统一处理

在上面的add方法中,如果遇到base竞争时,会使用cells记录更新值,在使用cells的的时候可能会遇到这些情况:

  1. 因为cells数组是在需要它的时候才会初始化的,可能cells数组还未初始化,此时需要初始化cells数组;
  2. 计算出来的cells数组某个索引位置的Cell对象还未初始化,此时需要初始化该位置的Cell对象;
  3. 在CAS的更新某个Cell对象的时候,又发生了冲突,即多个线程定位到了同一个Cell,此时可能需要对cells数组扩容。

面对这些情况,就需要调用longAccumulate方法进行统一的处理并最终更新给定值成功,该方法是Striped64的方法,LongAccumulator类中也是调用该方法。在LongAdder中调用该方法时,传递的参数为x、null、uncontended。

大概步骤就是:

  1. 调用getProbe获取此线程最新的probe赋值给h,如果为0,说明probe还没有初始化,即该线程第一次进入这个方法,那么进行如下操作:
    1. 调用ThreadLocalRandom.current()初始化线程的probe属性;
    2. 重新获取此时线程最新的probe赋值给h;
    3. 对于线程第一次进入的情况,如果是CAS失败的原因肯定是因为probe没有初始化才造成的CAS竞争数组0索引结点失败(0&m=0),此时wasUncontended为false。因此在probe初始化之后,将wasUncontended统一设置为true,表示“不存在CAS竞争”,因为觉得线程初始化之后通过这个probe找到的新索引位置是大概率不会CAS失败的。
  2. 初始化一个是否可能进行扩容的标志collide,如果为true,表示则下一次循环可能会进行扩容,如果为false,表示下一次循环一定不会进行扩容;
  3. 开启一个死循环,相当于自旋,尝试处理相应的情况并且并增加给定值。但是这里的自旋的效率相比于元素针对单个vlaue变量的效率高得多:
    1. as保存此时的cells数组,如果as不为null,说明cells数组已经初始化,并且n保存此时的as数组的长度,如果n大于0,说明数组初始化完毕:
      1. 通过(n - 1) & h定位到当前线程对应的某个Cell并使用a变量保存,这个定位方式就是类似于hash函数。
      2. 如果a为null,说明当前位置还没有线程使用过,那么尝试在该位置新增一个Cell,对于数组某个位置新增Cell的操作需要保证线程安全:
        1. 如果cellsBusy为0,表示当前没有其他线程在创建或扩容cells并且也没有创建Cell,即没有获取CAS锁:
          1. 新建一个Cell对象r,内部初始值就是x。
          2. 如果此时cellsBusy还是为0,那么当前线程调用casCellsBusy方法尝试将cellsBusy从0改为1,表示获取CAS锁。如果CAS成功那么可以进入下面的if代码块,用于进行对上面新创建的Cell进行赋值。这样就可以控制同一时刻只能有一个线程进入此if代码块,这里相当于借助cellsBusy和CAS操作实现了一个CAS锁,cellsBusy为0表示无锁,cellsBusy为1表示有线程加锁。
            1. created表示新建的Cell是否放入cells数组对应位置成功的标记,初始化为false,表示未成功。
            2. 重新获取此时最新的数组并计算该线程在数组中的对应的索引位置j,并判断如果该位置还是为null,那么将上面新建的Cell对象r,放入数组对应的j索引位置,created改为true,表示放入cells数组对应位置成功。
            3. 无论上面有没有成功,最终会将cellsBusy置为0,表示释放CAS锁。为什么获取CAS所致后还需要校验一次呢?因为可能在获取上面获取as之后加锁成功之前,底层的cells数组被其他线程改变了,比如被扩容,比如该位置被其他线程抢先存入了Cell对象或者扩容后计算出的位置本来就有Cell对象等,因此在加锁之后再一次检查是很有必要的。
            4. 最后判断created如果为true,表示放入cells数组对应位置成功,新增值x就是该位置新增元素的默认值,表示增加给定值成功,那么break跳出这个死循环,longAccumulate方法结束。
            5. 到这一步,即created为false,表示放入cells数组对应位置失败,对应的索引位置非null,那么continue结束本次循环,本次竞争到了锁说明竞争不是很激烈,下一次循环中有很大概率CAS成功,也不需要后续调用advanceProbe计算新的probe值了。
          3. 到这里,表示cellsBusy为1,即有线程正在创建cells,或者在扩容cells,或者在创建Cell;或者竞争CAS锁失败,collide置为false,下一次循环一定不会扩容,而是继续尝试。由于此时并发竞争可能会比较严重,随后会调用advanceProbe重新计算probe,并进行下一次循环,期望下一次循环时得到不同位置的Cell。
      3. 否则,a不为null,表示cells计算出来的索引位置已经存在Cell。如果wasUncontended为false。表示此线程的probe在前面已经被初始化并且是因为add方法中的a.cas调用失败才进来该方法的:
        1. wasUncontended重置为true,下一次循环中就可能会匹配到后面的条件。随后会调用advanceProbe重新计算probe,并进行下一次循环,期望下一次循环时得到不同位置的Cell。
      4. 否则,表示wasUncontended为true,可能是:上面的条件满足随后wasUncontended置为true并进行的第二次自旋,或者由于该线程getProbe()) == 0而在初始化probe之后将wasUncontended置为true。此时重新调用a.cas,尝试CAS的更新位置的Cell的值,如果CAS成功,那么表示增加给定值成功,break跳出循环,longAccumulate方法结束;
      5. 否则,表示上面的CAS更新失败,还是发生了CAS冲突。判断如果as数组长度n大于等于CPU的实际可用线程数量NCPU,表示达到了数组最大容量,不能够再扩容了,此后只能循环CAS直到成功;或者如果此时的cells不等于as了,表示数组被扩容了,那么同样需要重新尝试CAS操作新数组。
        1. 上面的条件满足一个,都会将collide设置为false,随后会调用advanceProbe重新计算probe,并进行下一次循环,期望下一次循环时得到不同位置的Cell。
      6. 否则,表示as数组长度n小于CPU的实际可用线程数量NCPU,并且此时的cells等于as了,表示没有数组没有扩容此时可以进行数组的扩容,但这里仅仅判断!collide是否为true,即collide是否为false:
        1. 如果collide为false,那么将collide设置为true。随后会调用advanceProbe重新计算probe,并进行下一次循环,期望下一次循环时得到不同位置的Cell。即,在真正尝试扩容之前,还需要再至少自旋一次,寄希望能够CAS成功,尽量避免扩容操作!
      7. 否则,表示collide为true。到这里,说明并发很严重,表示真正的可以扩容了。同样首先需要通过cellsBusy获取CAS锁,如果失败,随后会调用advanceProbe重新计算probe,并进行下一次循环,期望下一次循环时得到不同位置的Cell,如果成功获取CAS锁:
        1. 同样需要校验该数组是否是最新的数组,因为可能在加锁之前此数组被其他线程扩容了。如果是同一个数组,那么可以扩容:新建Cell数组,容量为原容量的两倍,循环旧数组,将数据转移到新数组对应的索引位置,将新数组赋给cells,到这里,表示扩容成功。
        2. 无论上面有没有扩容成功,最终会将cellsBusy置为0,表示释放CAS锁。
        3. 到这里,表示扩容成功,或者扩容失败此时是因为其他线程了扩容了数组,因此无论如何下一次循环都会使用新数组,collide重置为false。
        4. 由于扩容了数组,但是还没有增加给定值,因此还需要继续循环,但是由于后面循环使用更大的数组,因此将会有更大的概率CAS成功,直接continue结束本次循环,也不需要后续调用advanceProbe新的probe值了。
      8. 在该轮循环中数组已经初始化的情况下,如果没有在后续的代码中break跳出循环或则continue结束本次循环的操作,那么都将会调用advanceProbe重新计算当前线程的probe值,下一次循环时就有很大概率得到另外一个的Cell位置,可以减少下次访问cells元素时的冲突概率。
    2. 否则,表示数组没有初始化完毕,那么这里尝试进行数组的初始化,初始化操作同一时刻只能有一个线程进入,这里需要获取CAS锁。如果此时cellsBusy为0,那么表示当前没有其他线程在创建、扩容cells并且也没有创建Cell,但是有可能是其他线程已经初始化数组完毕了,并且如果此时cells 还是等于 as,说明数组确实没有被初始化,并且如果调用casCellsBusy方法尝试将cellsBusy从0改为1成功,那么表示获取了CAS锁,可以进入下面的代码块,用于进行数组的初始化:
      1. init表示新建数组成功的标记,初始化为false,表示未成功;
      2. 这里同样需要校验,因为可能在加锁之前此cells被其他线程初始化了,尽管这样的概率很低,如果通过,新建Cell数组,初始容量为2,通过 h&1 计算当前线程在该数组的位置,如果h的二进制数的最低位为1那么计算结果就是1索引,否为计算结果就是0索引,随后新建一个Cell对象初始化值就是给定值x,并放入计算出的数组对应的索引位置,将新数组rs赋给cells,到这里,表示初始化成功,同时增加给定值成功,init设置为true
      3. 无论上面有没有成功,最终会将cellsBusy置为0,表示释放CAS锁。
      4. 最后判断init如果为true,表示cells数组初始化同时增加给定值成功,那么break跳出这个死循环,longAccumulate方法结束。
      5. 到这一步还没有结束,表示cells数组初始化同时增加给定值失败,因为数组已被其他线程初始化了,随后会进行下一次循环;
    3. 否则,上面两个条件都不满足,表示其他线程正在初始化数组,或者数组已被其他线程初始化完毕,或者CAS竞争锁失败,那么还是尝试使用base变量来增加给定值,如果成功那么break跳出这个死循环,longAccumulate方法结束。可以看到,即使进入了longAccumulate方法,还有有可能会使用base更新的,那就是多个线程判断cells为null并同时进入的情况下。
    4. 到这里,表示casBase失败,那么继续下一次循环。

可以看到,源码还是比较复杂的,如果实在搞不懂的同学只需要记住我们前面讲的大概流程就行了!

/**
 * CPU中通常一个内核一个线程,后来有了超线程技术,可以把一个物理核心,模拟成两个逻辑核心,线程量增加一倍
 * 因此这里获取的是CPU的实际可用线程数量,比如 i7-8750H 它具有6核心12线程,因此获取的就是12而不是6
 * 通常CPU的实际可用线程数量越高,运行并发的程序的效率也越高
 * 

* CPU的实际可用线程数量NCPU在Striped64中被用来绑定cells数组的大小 */ static final int NCPU = Runtime.getRuntime().availableProcessors(); /** * 处理cells初始化、调整容量、创建新Cell等情况,并增加给定值。 * * @param x 给定值 * @param fn 累加规则函数,如果是LongAdder就传递null,表示仅仅是加法运算,如果是LongAccumulator就可以传递指定累加规则 * @param wasUncontended 如果在add方法的a.cas调用之前就调用该方法,那么为true,表示cells未初始化或者某个Cell未初始化;否则为false,表示a.cas竞争失败 */ final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) { int h; //获取此线程最新的probe赋值给h,如果为0,说明probe还没有初始化,即该线程第一次进入这个方法 if ((h = getProbe()) == 0) { //ThreadLocalRandom.current()方法本来是获取线程自己的随机数生成器,同时会初始化线程自己的probe和threadLocalRandomSeed属性 //这里被借用来初始化probe属性 ThreadLocalRandom.current(); // force initialization //重新获取此时线程最新的probe赋值给h h = getProbe(); //对于线程第一次进入的情况,如果是CAS失败的原因肯定是因为probe没有初始化才造成的CAS竞争数组0索引结点失败(0&m=0),此时wasUncontended为false //因此在probe初始化之后,将wasUncontended统一设置为true,表示“不存在CAS竞争”,因为觉得线程初始化之后通过这个probe找到的新索引位置是大概率不会CAS失败的 wasUncontended = true; } //是否可能进行扩容的标志,如果为true,表示则下一次循环可能会进行扩容,如果为false,表示下一次循环一定不会进行扩容 boolean collide = false; // True if last slot nonempty /* * 开启一个死循环,相当于自旋,尝试处理相应的情况并且并增加给定值 * 但是这里的自旋的效率相比于元素针对单个vlaue变量的效率高得多 */ for (; ; ) { Cell[] as; Cell a; int n; long v; /* * as保存此时的cells,如果as不为null,说明cells数组已经初始化 * 并且 n保存此时的as数组的长度,如果n大于0,说明数组初始化完毕 */ if ((as = cells) != null && (n = as.length) > 0) { /* * 通过(n - 1) & h定位到当前线程对应的某个Cell并使用a变量保存,这个定位方式就是类似于hash函数 * 如果a为null,说明当前位置还没有线程使用过,那么尝试在该位置新增一个Cell,对于数组某个位置新增Cell的操作需要保证线程安全 */ if ((a = as[(n - 1) & h]) == null) { //如果cellsBusy为0,表示当前没有其他线程在创建或扩容cells并且也没有创建Cell //即没有获取CAS锁,此时需要新建一个Cell放到该位置 if (cellsBusy == 0) { // Try to attach new Cell //新建一个Cell对象r,内部初始值就是x Cell r = new Cell(x); // Optimistically create /* * 如果此时cellsBusy还是为0,那么表示当前没有其他线程在创建或扩容cells并且也没有创建Cell * 然后当前线程调用casCellsBusy方法尝试将cellsBusy从0改为1,表示获取CAS锁 * 如果CAS成功那么可以进入下面的if代码块,用于进行对上面新创建的Cell进行赋值 * 如果CAS失败,说明此时有线程正在操作cells数组,那么不会进if代码块 * 同一时刻只能有一个线程进入此if代码块,这里相当于借助cellsBusy和CAS操作实现了一个CAS锁 * cellsBusy为0表示无锁,cellsBusy为1表示有线程加锁 */ if (cellsBusy == 0 && casCellsBusy()) { //新建的Cell是否放入cells数组对应位置成功的标记,初始化为false表示未成功 boolean created = false; //放置Cell的操作 try { // Recheck under lock Cell[] rs; int m, j; /* * 这里相当于再检查一遍,为什么要检查呢,如果可能在上面获取as之后加锁成功之前,底层的cells数组被其他线程改变了 * 比如被扩容,比如该位置被其他线程抢先存入了Cell对象或者扩容后计算出的位置本来就有Cell对象等,因此在加锁之后再一次检查是很有必要的 * * rs保存此时的cells,如果rs不为null,说明cells数组已经初始化 * 并且 m保存此时的rs数组的长度,如果m大于0,说明数组初始化完毕 * 并且 j保存前线程定位到的Cell索引,该索引的元素为null,说明当前位置还没有线程使用过 * * 这三个条件都满足,那么进入if代码块 * 如果3个条件有一个不满足,那么不进入if代码块,将会进行下一次循环 */ if ((rs = cells) != null && (m = rs.length) > 0 && rs[j = (m - 1) & h] == null) { //将上面新建的Cell对象r,放入数组对应的j索引位置 rs[j] = r; //created改为true,表示放入cells数组对应位置成功 created = true; } } finally { //无论上面有没有成功,最终会将cellsBusy置为0,表示释放CAS锁 cellsBusy = 0; } //最后判断created如果为true,表示放入cells数组对应位置成功, //新增值x就是该位置新增元素的默认值,表示增加给定值成功,那么break跳出这个死循环,longAccumulate方法结束 if (created) break; //到这一步,即created为false,表示放入cells数组对应位置失败,对应的索引位置非null,那么continue结束本次循环, //本次竞争到了锁说明竞争不是很激烈,下一次循环中有很大概率CAS成功,也不需要后续调用advanceProbe计算新的probe值了 continue; // Slot is now non-empty } } //到这里,表示cellsBusy为1,即有线程正在创建cells,或者在扩容cells,或者在创建Cell;或者竞争CAS锁失败 //collide置为false,下一次循环一定不会扩容,而是继续尝试 collide = false; //由于此时并发竞争可能会比较严重,随后会调用advanceProbe重新计算probe,并进行下一次循环,期望下一次循环时得到不同位置的Cell } /* * 否则,a不为null,表示cells计算出来的索引位置已经存在Cell,如果wasUncontended为false * 表示此线程的probe在前面已经被初始化并且是因为add方法中的a.cas调用失败才进来该方法的 */ else if (!wasUncontended) // CAS already known to fail //wasUncontended重置为true,下一次循环中就可能会匹配到后面的条件 //随后会调用advanceProbe重新计算probe,并进行下一次循环,期望下一次循环时得到不同位置的Cell wasUncontended = true; // Continue after rehash /* * 否则,表示wasUncontended为true,可能是: * 上面的!wasUncontended条件满足随后wasUncontended置为true并进行的第二次自旋 * 或者由于该线程getProbe()) == 0而在初始化probe之后将wasUncontended置为true * * 此时重新调用a.cas,尝试CAS的更新该位置的Cell的值,如果CAS成功,那么表示增加给定值成功,break跳出循环,longAccumulate方法结束 */ else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x)))) break; /* * 否则,表示上面的CAS更新失败,说明还是发生了CAS冲突 * 判断如果as数组长度n大于等于CPU的实际可用线程数量NCPU,表示达到了数组最大容量,不能够再扩容了,此后只能循环CAS直到成功 * 或者如果此时的cells不等于as了,表示数组被扩容了,那么同样需要重新尝试CAS操作新数组 * * 上面的条件满足一个,都会将collide设置为false,随后会调用advanceProbe重新计算probe,并进行下一次循环,期望下一次循环时得到不同位置的Cell */ else if (n >= NCPU || cells != as) collide = false; // At max size or stale /* * 否则,表示as数组长度n小于CPU的实际可用线程数量NCPU,并且此时的cells等于as了,表示没有数组没有扩容 * 此时可以进行数组的扩容,但这里仅仅判断如果!collide为true,即collide为false,那么将collide设置为true, * 随后会调用advanceProbe重新计算probe,并进行下一次循环,期望下一次循环时得到不同位置的Cell * 即,在真正尝试扩容之前,还需要再至少自旋一次,寄希望能够CAS成功,尽量避免扩容操作 * * * collide设置为true表示下一次循环可能进行扩容,但也不是一定的,下一次循环过程中,可能遇到: * 数组被扩容了,计算的新位置没有Cell,此时该线程会存放Cell,并跳出循环 * CAS成功,跳出循环 * 遇到n >= NCPU,即数组不能再扩容了,那么该线程的扩容操作就会被取消,collide = false,此后会一致在这几个操作中循环直到CAS成功 * cells != as,即数组又被扩容了,那么还会进入下一次循环,collide = false,对新数组进行尝试 * * 前面的都不满足,此时由于上一次循环中将collide设置为了true,因此这里的!collide不满足,终于将会进入下面的else if条件,尝试进行数组的扩容 */ else if (!collide) collide = true; /* * 否则,表示collide为true。到这里,说明并发很严重,表示真正的可以扩容了 * 同样首先需要通过cellsBusy获取CAS锁,如果失败,随后会调用advanceProbe重新计算probe,并进行下一次循环,期望下一次循环时得到不同位置的Cell * 但是这里collide没有被重置为false */ else if (cellsBusy == 0 && casCellsBusy()) { //加锁成功之后 try { //同样需要校验该数组是否是最新的数组,因为可能在加锁之前此数组被其他线程扩容了 if (cells == as) { // Expand table unless stale //新建Cell数组,容量为原容量的两倍 Cell[] rs = new Cell[n << 1]; //循环旧数组,将数据转移到新数组对应的索引位置 for (int i = 0; i < n; ++i) rs[i] = as[i]; //将新数组赋给cells,到这里,表示扩容成功 cells = rs; } } finally { //无论上面有没有成功,最终会将cellsBusy置为0,表示释放CAS锁 cellsBusy = 0; } //到这里,表示扩容成功,或者扩容失败此时是因为其他线程了扩容了数组,因此无论如何下一次循环都会使用新数组 //collide重置为false collide = false; //由于扩容了数组,但是还没有增加给定值,因此还需要继续循环,但是由于后面循环使用更大的数组,因此将会有更大的概率CAS成功 //直接continue结束本次循环,也不需要后续调用advanceProbe新的probe值了 continue; // Retry with expanded table } /* * 该轮循环中数组已经初始化的情况下,如果没有在后续的代码中break跳出循环或则continue结束本次循环的操作,那么都将会重新计算当前线程的probe值, * 下一次循环时就有很大概率得到另外一个的Cell位置,可以减少下次访问cells元素时的冲突概率。 */ //使用xorshift算法生成随机数 h = advanceProbe(h); } /* * 否则,表示数组没有初始化,那么这里尝试进行数组的初始化,初始化操作同一时刻只能有一个线程进入,这里需要获取CAS锁 * * 如果 此时cellsBusy为0,那么表示当前没有其他线程在创建、扩容cells并且也没有创建Cell,但是有可能是其他线程已经初始化数组完毕了 * 并且 如果此时cells 还是等于 as,说明数组确实没有被初始化 * 并且 如果调用casCellsBusy方法尝试将cellsBusy从0改为1成功,那么可以进入下面的代码块,用于进行数组的初始化 * * 如果上面的条件都满足,那么可以尝试进行数组的初始化,否则表示数组已经被初始化了,将会进入最后一个else if条件 */ else if (cellsBusy == 0 && cells == as && casCellsBusy()) { //新建数组成功的标记,初始化为false表示未成功 boolean init = false; //初始化数组的操作 try { // Initialize table //同样需要校验,因为可能在加锁之前此数组被其他线程初始化了,尽管这样的概率很低 if (cells == as) { //新建Cell数组,初始容量为2 Cell[] rs = new Cell[2]; //通过 h&1 计算当前线程在该数组的位置,如果h的二进制数的最低位为1那么计算结果就是1索引,否为计算结果就是0索引 //随后新建一个Cell对象初始化值就是给定值x,并放入计算出的数组对应的索引位置 rs[h & 1] = new Cell(x); //将新数组rs赋给cells,到这里,表示初始化成功,同时增加给定值成功 cells = rs; //init设置为true init = true; } } finally { //无论上面有没有成功,最终会将cellsBusy置为0,表示释放CAS锁 cellsBusy = 0; } //最后判断init如果为true,表示cells数组初始化同时增加给定值成功,那么break跳出这个死循环,longAccumulate方法结束 if (init) break; //到这一步还没有结束,表示cells数组初始化同时增加给定值失败,因为数组已被其他线程除除四化了,随后会进行下一次循环 } /* * 否则,上面两个条件都不满足,表示其他线程正在初始化数组,或者数组已被其他线程初始化完毕,或者CAS竞争锁失败 * 那么还是尝试使用base变量来增加给定值,如果成功那么break跳出这个死循环,longAccumulate方法结束 * 可以看到,即使进入了longAccumulate方法,还有有可能会使用base更新的,那就是多个线程判断cells为null并同时进入的情况下 */ else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x)))) break; // Fall back on using base //到这里,表示casBase失败,那么继续下一次循环 } } /** * 尝试CAS的将cellsBusy值从0更新为1,表示获取了CAS锁 */ final boolean casCellsBusy() { return UNSAFE.compareAndSwapInt(this, CELLSBUSY, 0, 1); } /** * 使用xorshift算法基于当前probe伪随机的生成下一个probe值 */ static final int advanceProbe(int probe) { probe ^= probe << 13; // xorshift probe ^= probe >>> 17; probe ^= probe << 5; UNSAFE.putInt(Thread.currentThread(), PROBE, probe); return probe; }

1.1.2.3 increment自增

public void decrement()

自增1,内部就是调用的add(1L)方法。

/**
 * 自增1,内部调用add方法,参数为1
 */
public void increment() {
    add(1L);
}
1.1.2.4 sum统计

public long sum()

返回当前总和。其实是base 的值与Cells 数组里面所有Cell元素中的value 值的累加。可以看到仅仅对base和cells数组元素value的简单累加,因此这个sum可能不是最新值,即不准确。

/**
 * 返回当前总和。其实是base 的值与Cells 数组里面所有Cell元素中的value 值的累加。
 *
 * @return 总和,非强一致性的
 */
public long sum() {
    //as保存此时的cells数组
    Cell[] as = cells;
    Cell a;
    //初始化sum,保存base
    long sum = base;
    //如果as不为null
    if (as != null) {
        //那么循环as数组将每一个元素的value相加,由于没有加CAS锁,此时的as可能不是最新的cells数组了
        for (int i = 0; i < as.length; ++i) {
            //如果某个索引位置元素不为nuull
            if ((a = as[i]) != null)
                //那么对sum进行累加
                sum += a.value;
        }
    }
    //返回sum
    return sum;
}

longValue方法内部也是调用了sum方法:

public long longValue() {
    return sum();
}
1.1.2.5 reset重置

public void reset()

reset为重置操作,将保持总和的变量重置为零。即base值置为0,如果有cells数组,则将每一个元素(如果存在)的value值置为0。

该方法返回之后不代表此时总和一定为0,因为可能前面刚刚将某个位置的值置为0,后面马上被其他线程增加了值,因此这个方法也没有任何保证。

/**
 * reset为重置操作,将保持总和的变量重置为零。
 * 即base值置为0,如果有cells数组,则将每一个元素(如果存在)的value值置为0 。
 */
public void reset() {
    //as保存此时的cells数组
    Cell[] as = cells;
    Cell a;
    //base重置为0
    base = 0L;
    //如果as不为null
    if (as != null) {
        //那么循环as数组将每一个元素的value置为0,由于没有加CAS锁,此时的as可能不是最新的cells数组了
        for (int i = 0; i < as.length; ++i) {
            //如果某个索引位置元素不为nuull
            if ((a = as[i]) != null)
                //该位置元素的value置为0
                a.value = 0L;
        }
    }
}
1.1.2.6 sumThenReset统计并重置

相当于sum()后跟reset()。

/**
 * 相当于sum()后跟reset()。
 *
 * @return 总和
 */
public long sumThenReset() {
    //as保存此时的cells数组
    Cell[] as = cells;
    Cell a;
    //初始化sum,保存base
    long sum = base;
    //base重置为0
    base = 0L;
    //如果as不为null
    if (as != null) {
        //那么循环as数组将每一个元素的value值相加,随后置为0,由于没有加CAS锁,此时的as可能不是最新的cells数组了
        for (int i = 0; i < as.length; ++i) {
            //如果某个索引位置元素不为null
            if ((a = as[i]) != null) {
                //那么对sum进行累加
                sum += a.value;
                //该位置元素的value置为0
                a.value = 0L;
            }
        }
    }
    //返回sum
    return sum;
}

2 JMH性能测试

下面我们测试LongAdder和AtomicLong的性能差别,我们使用JMH方法性能测试。Java使用JMH进行方法性能优化测试。

本次主要测试AtomicLong的getAndIncrement方法、LongAdder的increment方法以及使用synchronized同步的方法在1秒钟之内能调用多少次,即测试方法的吞吐量。

@OutputTimeUnit(TimeUnit.MICROSECONDS)
@BenchmarkMode(Mode.Throughput)
public class AdderJMHTest {

    private static AtomicLong count = new AtomicLong();
    private static LongAdder longAdder = new LongAdder();
    private static long syn = 0L;

    public static void main(String[] args) throws Exception {
        Options options = new OptionsBuilder().include(AdderJMHTest.class.getName()).warmupIterations(1).measurementIterations(2).forks(1).build();
        new Runner(options).run();
    }

    @Benchmark
    @Threads(10)
    public void run0() {
        count.getAndIncrement();
    }

    @Benchmark
    @Threads(10)
    public void run1() {
        longAdder.increment();
    }

    @Benchmark
    @Threads(10)
    public void run2() {
        synchronized (AdderJMHTest.class) {
            ++syn;
        }
    }
}

在开启JIT优化的情况下,开启1个线程,结果如下:

Benchmark           Mode  Cnt    Score   Error   Units
AdderJMHTest.run0  thrpt    2  155.021          ops/us
AdderJMHTest.run1  thrpt    2  124.791          ops/us
AdderJMHTest.run2  thrpt    2   57.716          ops/us

在开启JIT优化的情况下,开启2个线程,结果如下:

Benchmark           Mode  Cnt    Score   Error   Units
AdderJMHTest.run0  thrpt    2   56.432          ops/us
AdderJMHTest.run1  thrpt    2  243.411          ops/us
AdderJMHTest.run2  thrpt    2   32.125          ops/us

在开启JIT优化的情况下,开启5个线程,结果如下:

Benchmark           Mode  Cnt    Score   Error   Units
AdderJMHTest.run0  thrpt    2   52.174          ops/us
AdderJMHTest.run1  thrpt    2  486.320          ops/us
AdderJMHTest.run2  thrpt    2   36.689          ops/us

在开启JIT优化的情况下,开启10个线程,结果如下:

Benchmark           Mode  Cnt    Score   Error   Units
AdderJMHTest.run0  thrpt    2   48.207          ops/us
AdderJMHTest.run1  thrpt    2  756.315          ops/us
AdderJMHTest.run2  thrpt    2   31.929          ops/us

在开启JIT优化的情况下,开启20个线程,结果如下:

Benchmark           Mode  Cnt    Score   Error   Units
AdderJMHTest.run0  thrpt    2   50.508          ops/us
AdderJMHTest.run1  thrpt    2  791.501          ops/us
AdderJMHTest.run2  thrpt    2   36.743          ops/us

在开启JIT优化的情况下,开启50个线程,结果如下:

Benchmark           Mode  Cnt    Score   Error   Units
AdderJMHTest.run0  thrpt    2   52.193          ops/us
AdderJMHTest.run1  thrpt    2  800.270          ops/us
AdderJMHTest.run2  thrpt    2   31.817          ops/us

在开启JIT优化的情况下,开启100个线程,结果如下:

Benchmark           Mode  Cnt    Score   Error   Units
AdderJMHTest.run0  thrpt    2   51.982          ops/us
AdderJMHTest.run1  thrpt    2  842.155          ops/us
AdderJMHTest.run2  thrpt    2   34.325          ops/us

可见,synchronized吞吐量最少。如果没有线程竞争,那么LongAdder和AtomicLong的吞吐量差不多。如果线程竞争较多,那么AtomicLong吞吐量降低,LongAdder吞吐量继续升高,是AtomicLong的是十倍以上。

在使用-Xint参数关闭JIT优化的情况下,开启10个线程,结果如下:

Benchmark           Mode  Cnt   Score   Error   Units
AdderJMHTest.run0  thrpt    2   4.781          ops/us
AdderJMHTest.run1  thrpt    2  15.816          ops/us
AdderJMHTest.run2  thrpt    2   3.925          ops/us

虽然它们的总体性能都严重降低,但是LongAdder的吞吐量仍然最大,这也说明Java的JIT优化的牛逼之处。

3 atomic的总结

JDK1.8新增的LongAdder,正是为了解决高并发环境下由于频繁读写AtomicLong等计数器而可能造成某些线程持续的空转(循环)进而浪费CPU的情况,它们也被称为“累加器”,其“热点数据分离”的基本思想非常值得我们学习!

相关文章:

  1. Unsafe:JUC—Unsafe类的原理详解与使用案例。
  2. volatile:Java中的volatile实现原理深度解析以及应用。
  3. CAS:Java中的CAS实现原理深度解析与应用案例。
  4. 伪共享:Java中的伪共享深度解析以及避免方法。
  5. JMH:Java使用JMH进行方法性能优化测试。

如果有什么不懂或者需要交流,可以留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!

你可能感兴趣的:(Java,JUC源码,java,juc,并发编程,LongAdder)