JDK8 新特性 LongAdder 源码解析

JDK8 新特性 LongAdder 源码解析

原子累加器

  • LongAdder
  • DoubleAdder
  • LongAccumulator
  • DoubleAccumulator

jdk8 以后,新增了几个专门用来做累加的类,它们累加的性能要比 Atomic 类高很多。

累加器性能比较

/**
 * Atomic和LongAdder耗时测试
 */
public class Main {
    public static void main(String[] args) throws Exception{
        testAtomicLongAdder(1, 10000000); // 1个线程 共累加1千万次
        testAtomicLongAdder(10, 10000000); // 10个线程 共累加1千万次
        testAtomicLongAdder(100, 10000000); // 100个线程 共累加1千万次
    }

    static void testAtomicLongAdder(int threadCount, int times) throws Exception{
        System.out.println("threadCount: " + threadCount + ", times: " + times);
        long start = System.currentTimeMillis();
        testLongAdder(threadCount, times);
        System.out.println("LongAdder 耗时:" + (System.currentTimeMillis() - start) + "ms");
        System.out.println("threadCount: " + threadCount + ", times: " + times);
        long atomicStart = System.currentTimeMillis();
        testAtomicLong(threadCount, times);
        System.out.println("AtomicLong 耗时:" + (System.currentTimeMillis() - atomicStart) + "ms");
        System.out.println("----------------------------------------");
    }

    static void testAtomicLong(int threadCount, int times) throws Exception{
        AtomicLong atomicLong = new AtomicLong();
        List<Thread> list = new ArrayList<>();
        for (int i = 0; i < threadCount; i++) {
            list.add(new Thread(() -> {
                for (int j = 0; j < times; j++) {
                    atomicLong.incrementAndGet();
                }
            }));
        }

        for (Thread thread : list) {
            thread.start();
        }

        for (Thread thread : list) {
            thread.join();
        }

        System.out.println("AtomicLong value is : " + atomicLong.get());
    }

    static void testLongAdder(int threadCount, int times) throws Exception{
        LongAdder longAdder = new LongAdder();
        List<Thread> list = new ArrayList<>();
        for (int i = 0; i < threadCount; i++) {
            list.add(new Thread(() -> {
                for (int j = 0; j < times; j++) {
                    longAdder.increment();
                }
            }));
        }

        for (Thread thread : list) {
            thread.start();
        }

        for (Thread thread : list) {
            thread.join();
        }

        System.out.println("LongAdder value is : " + longAdder.longValue());
    }
}

输出

threadCount: 1, times: 10000000
LongAdder value is : 10000000
LongAdder 耗时:181ms
threadCount: 1, times: 10000000
AtomicLong value is : 10000000
AtomicLong 耗时:106ms
----------------------------------------
threadCount: 10, times: 10000000
LongAdder value is : 100000000
LongAdder 耗时:364ms
threadCount: 10, times: 10000000
AtomicLong value is : 100000000
AtomicLong 耗时:1769ms
----------------------------------------
threadCount: 100, times: 10000000
LongAdder value is : 1000000000
LongAdder 耗时:1500ms
threadCount: 100, times: 10000000
AtomicLong value is : 1000000000
AtomicLong 耗时:17597ms
----------------------------------------

在线程数只有 1 个的时候,LongAdder 并未体现出优势;

但随着线程数的增加,LongAdder 和 AtomicLong 的差距就相当明显了,AtomicLong 的性能急剧下降,耗时是 LongAdder 的数倍。

由此可看出 LongAdder 在多线程高并发下的数据统计性能非常优秀。

性能提升的原因很简单,就是在有竞争时,设置多个累加单元,Therad-0 累加 Cell[0],而 Thread-1 累加 Cell[1]… 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性能。

LongAdder 和 LongAccumulator 区别:

  • 相同点:

    • LongAdder 与 LongAccumulator 类都是使用非阻塞算法 CAS 实现的
    • LongAdder 类是 LongAccumulator 类的一个特例,只是 LongAccumulator 提供了更强大的功能,可以自定义累加规则,当LongBinaryOperator accumulatorFunction 为 null 时就等价于 LongAdder
  • 不同点:

    • 调用 casBase 时

      • LongAccumulator 使用 function.applyAsLong(b = base, x) 来计算
      • LongAdder 使用 casBase(b = base, b + x) 来计算
    • LongAccumulator 类功能更加强大,构造方法参数中

      • LongBinaryOperator accumulatorFunction 是一个双目运算器接口,可以指定累加规则,比如累加或者相乘,其根据输入的两个参数返回一个计算值,LongAdder 内置累加规则
      • identity 则是 LongAccumulator 累加器的初始值,LongAccumulator 可以为累加器提供非 0 的初始值,而 LongAdder 只能提供默认的 0

LongAccumulator 和LongAdder 的实现方式是完全一样的,只是做了一些定制。通过 LongBinaryOperator 函数,后面我们在看源码的时候会看到,二者唯一的不同就是计算是用的值还是用的 LongBinaryOperator 来计算。LongAccumulator 性能慢一些,是因为 Lambda 表达式(方法引用)会额外的创建一些对象。

LongAdder简介

优化机制

LongAdder 是 Java8 提供的类,跟 AtomicLong 有相同的效果,但对 CAS 机制进行了优化,尝试使用分段 CAS 以及自动分段迁移的方式来大幅度提升多线程高并发执行 CAS 操作的性能。

CAS 底层实现是在一个循环中不断地尝试修改目标值,直到修改成功。如果竞争不激烈修改成功率很高,否则失败率很高,失败后这些重复的原子性操作会耗费性能(导致大量线程空循环,自旋转)。

优化核心思想:数据分离,将 AtomicLong 的单点的更新压力分担到各个节点,空间换时间,在低并发的时候直接更新,可以保障和 AtomicLong 的性能基本一致,而在高并发的时候通过分散减少竞争,提高了性能。

分段 CAS 机制

  • 在发生竞争时,创建 Cell 数组用于将不同线程的操作离散(通过 hash 等算法映射)到不同的节点上
  • 设置多个累加单元(会根据需要扩容,最大为 CPU 核数),Therad-0 累加 Cell[0],而 Thread-1 累加 Cell[1] 等,最后将结果汇总
  • 在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性能

自动分段迁移机制:某个 Cell 的 value 执行 CAS 失败,就会自动寻找另一个 Cell 分段内的 value 值进行 CAS 操作。


伪共享

一个缓存行加入了多个 Cell 对象叫做伪共享。

Cell 为累加单元:数组访问索引是通过 Thread 里的 threadLocalRandomProbe 域取模实现的,这个域是 ThreadLocalRandom 更新的

// Striped64.Cell
// @sun.misc.Contended:防止缓存行伪共享
@sun.misc.Contended static final class Cell {
    volatile long value;
    Cell(long x) { value = x; }
    // 最重要的方法 用 cas 方式进行累加, prev 表示旧值, next 表示新值
    final boolean cas(long prev, long next) {
    	return UNSAFE.compareAndSwapLong(this, valueOffset, prev, next);
    }
    // 省略不重要代码
}

得从缓存说起

缓存与内存的速度比较

JDK8 新特性 LongAdder 源码解析_第1张图片

cpu 大约需要的时钟周期
寄存器 1 cycle (4GHz 的 CPU 约为0.25ns)
L1 3~4 cycle
L2 10~20 cycle
L3 40~45 cycle
内存 120~240 cycle
  • 因为 CPU 与 内存的速度差异很大,需要靠预读数据至缓存来提升效率。
  • 而缓存以缓存行为单位,每个缓存行对应着一块内存,一般是 64 byte(8 个 long)
  • 缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中
  • CPU 要保证数据的一致性,如果某个 CPU 核心更改了数据,其它 CPU 核心对应的整个缓存行必须失效

JDK8 新特性 LongAdder 源码解析_第2张图片

因为 Cell 是数组形式,在内存中是连续存储的,64 位系统中,一个 Cell 为 24 字节(16 字节的对象头和 8 字节的 value),每一个 cache line 为 64 字节,因此缓存行可以存下 2 个的 Cell 对象。这样问题来了:

  • Core-0 要修改 Cell[0]
  • Core-1 要修改 Cell[1]

无论谁修改成功,都会导致对方 Core 的缓存行失效,比如 Core-0 中 Cell[0]=6000, Cell[1]=8000 要累加 Cell[0]=6001, Cell[1]=8000 ,这时会让 Core-1 的缓存行失效。

@sun.misc.Contended 用来解决这个问题,它的原理是在使用此注解的对象或字段的前后各增加 128 字节大小的 padding,从而让 CPU 将对象预读至缓存时占用不同的缓存行,这样,不会造成对方缓存行的失效。

JDK8 新特性 LongAdder 源码解析_第3张图片


LongAdder图解

  • LongAdder 的结构比较简单,如下图

    JDK8 新特性 LongAdder 源码解析_第4张图片

  • 其由 base 值 (long类型) 和 Cell 数组构成,如上图

  • 当没有 base 的更新没有线程竞争的时候,会直接写到 base 里面去,而不会操作 Cell 数组,当 base 的写出现了竞争的时候,就会创建 Cell 数组,由不同的线程写不同的下标。当最后求和的时候,通过上述的公式,sum = base + 所有槽的值

    s u m = b a s e + ∑ i = 0 n C e l l [ i ] sum=base+\sum_{i=0}^{n} Cell[i] sum=base+i=0nCell[i]

  • 执行流程图

    JDK8 新特性 LongAdder 源码解析_第5张图片

  • 刚开始第一次去理解上述的内容比较抽象,现在从源码开始讲起。

LongAdder源码

属性

  • LongAdder 的属性,在其父类 Striped64 中

    abstract class Striped64 extends Number {
    
        /*
         * @sun.misc.Contended 添加缓存填充,用来消除伪共享
         * 伪共享会导致缓存行失效,缓存一致性开销变大。
         * 伪共享指的是多个线程同时读写同一个缓存行的不同变量时导致的 CPU缓存失效。
         * 尽管这些变量之间没有任何关系,但由于在主内存中邻近,存在于同一个缓存行之中,
         * 它们的相互覆盖会导致频繁的缓存未命中,引发性能下降。这里对于伪共享我只是提一下概念,并不会深入去讲解,大家可以自行查阅一些资料。
         * 解决伪共享的方法一般都是使用直接填充,我们只需要保证不同线程的变量存在于不同的 CacheLine 即可,
         * 使用多余的字节来填充可以做点这一点,这样就不会出现伪共享问题。在Disruptor队列的设计中就有类似设计。
         */
        @sun.misc.Contended static final class Cell {
            volatile long value;
            Cell(long x) { value = x; }
            final boolean cas(long cmp, long val) {
                return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
            }
    
            // Unsafe mechanics
            private static final sun.misc.Unsafe UNSAFE;
            private static final long valueOffset; // 偏移量
            static {
                try {
                    UNSAFE = sun.misc.Unsafe.getUnsafe();
                    Class<?> ak = Cell.class; // 拿到Cell对象
                    valueOffset = UNSAFE.objectFieldOffset // 获取偏移地址
                            (ak.getDeclaredField("value")); // 获取value字段
                } catch (Exception e) {
                    throw new Error(e);
                }
            }
        }
    
        /** Number of CPUS, to place bound on table size */
        // 表示当前计算机CPU核心数,作用 => 控制cells数组长度的一个关键条件
        static final int NCPU = Runtime.getRuntime().availableProcessors();
    
        /**
         * Table of cells. When non-null, size is a power of 2.
         * Cells 数组 大小是2的幂
         */
        transient volatile Cell[] cells;
    
        /**
         * Base value, used mainly when there is no contention, but also as
         * a fallback during table initialization races. Updated via CAS.
         * base 基础 value 值,当并发较低的时候,只累加该值,主要用于没有竞争的情况,通过CAS更新 | 当cells扩容时,需要将数据写入base中
         */
        transient volatile long base;
    
        /**
         * Spinlock (locked via CAS) used when resizing and/or creating Cells.
         * 创建或者落入Cells数组的时候使用的自旋锁变量调整单元格大小,创建单元格时候使用的锁
         * 初始化cells或者扩容cells需要获取锁 0表示无锁状态 1表示其他线程已经持有了锁
         */
        transient volatile int cellsBusy;
    }
    

工作流程:

  • cells 占用内存是相对比较大的,是惰性加载的,在无竞争或者其他线程正在初始化 cells 数组的情况下,直接更新 base 域

  • 在第一次发生竞争时(casBase 失败)会创建一个大小为 2 的 cells 数组,将当前累加的值包装为 Cell 对象,放入映射的槽位上

  • 分段累加的过程中,如果当前线程对应的 cells 槽位为空,就会新建 Cell 填充,如果出现竞争,就会重新计算线程对应的槽位,继续自旋尝试修改

  • 分段迁移后还出现竞争就会扩容 cells 数组长度为原来的两倍,然后 rehash,数组长度总是 2 的 n 次幂,默认最大为 CPU 核数,但是可以超过,如果核数是 6 核,数组最长是 8

add()

  • LongAdder#add()方法

    我们通常使用的是 LongAdder 的 increment() 方法,也就是自增方法,底层 LongAdder 其实调用的是 add() 方法

    public class LongAdder extends Striped64 implements Serializable {
        /**
         * Equivalent to {@code add(1)}.
         * 自增 实际上调用的是 add(1L);
         */
        public void increment() {
            add(1L);
        }
        
        /**
         * Equivalent to {@code add(-1)}.
         * 自减 实际上调用的是 add(1L);
         */
        public void decrement() {
            add(-1L);
        }
    }
    
  • LongAdder继承Striped64,并且使用到了Striped64中的方法,这是一个公共方法,我们稍后再说。

  • 现在来分析 LongAdder 的 add() 方法

    public class LongAdder extends Striped64 implements Serializable {
        /**
         * Adds the given value.
         * 1.casBase
         * 2.casBase失败,创建cells
         * 3.cells失败,扩容cells,扩容到CPU核心数大小
         * @param x the value to add
         */
        public void add(long x) {
            // as 表示当前cells引用
            // b 表示获取的base值
            // v 表示期望值
            // m 表示cells数组的长度
            // a 表示当前线程命中的cell单元格
            Cell[] as; long b, v; int m; Cell a;
            // 条件1:true->表示cell已经初始化过了 当前线程应该将数据写入到对应的cell中
            // 		  false->表示第一次加载,cells未初始化,所有的数据应该尝试写到base中
            // 条件2:true->表示当前线程CAS数据替换失败,发生竞争了,可能需要重试 或者 扩容,这里指的是 !casBase(x,y) 整体是否为true
            // 		  false->表示当前线程CAS成功
            if ((as = cells) != null || !casBase(b = base, b + x)) {
                // 什么时候会进来?
                // 1.true->cells已经初始化,需要尝试将数据写入到对应的cell,注意,这里是尝试写入
                // 2.true->发生竞争了,可能需要重试 或 扩容
    
                // ture->未竞争 false->发生竞争
                boolean uncontended = true;
    
                // 第一次 as 为null
    
                // 已经新建了数组了
                // 如果某个calls槽cas失败,就可能进行扩容操作
                // 如果第一次写发现值为null,初始化了cell,但是没有设置值。需要设置值
                // 如果不为null,就进行cas操作
                // 如果不为null,就进行cas操作 且 cas操作失败,然后扩容
    
                // 条件1:as == null true->说明当前cells未初始化,也就是多线程写base发生竞争了
                //  				 false->说明当前cells已经始化了,当前线程应该是 找自己的cell 写值
                // 条件2:(m = as.length - 1) < 0
                //        true->当前cells数组长度为0
                //        false->当前cells数组长度大于0
                //        m = cells数组长度,且长度一定为2的幂,后续会看到或参考HashMap的扩容
                // 条件3:(a = as[getProbe() & m]) == null 
                //		  true->当前线程的cells[index]对应的数据为null,说明是第一次写值,需要创建 longAccumulate 支持
                //        false->说明当前线程对应的cell不为空,说明 下一步想要将x值 添加到cell中
                //        捞一下as[getProbe() & m]:获取当前线程的hash值 与上 cells长度-1(m,上一个条件计算的),计算cells中的下标
                // 条件4:!(uncontended = a.cas(v = a.value, v + x)) 
                //        true->当前 a = cells[index] cas写失败 此时 uncontended = false 发生竞争了
                //        false->表示cas成功
                if (as == null || (m = as.length - 1) < 0 ||
                    (a = as[getProbe() & m]) == null ||
                    !(uncontended = a.cas(v = a.value, v + x)))
                    // 都有哪些情况会调用?
                    // 1.true->说明当前cells未初始化,也就是多线程写base发生竞争了[重试|初始化cells]
                    // 2.true->当前cells数组长度为0
                    // 3.true->当前线程的cells[index]对应的数据为null,说明是第一次写值,需要创建 longAccumulate 支持
                    // 4.true->当前 a = cells[index] cas写失败 此时 uncontended = false 发生竞争了[重试|扩容]
                    // 执行累加 调用父类Striped64的longAccumulate方法
                    longAccumulate(x, null, uncontended);
            }
        }
    }
    

    流程图

    JDK8 新特性 LongAdder 源码解析_第6张图片

Striped64中一些操作方法

  • Striped64 提供了数据的 CAS 操作,和线程的 hash 处理,如下

    abstract class Striped64 extends Number {
    
        /**
         * CASes the base field.
         */
        final boolean casBase(long cmp, long val) {
            // cas 进行更新 base值
            // 返回是否更新成功,CAS只写一次
            // 可能写失败,写失败就开始建立Cells
            return UNSAFE.compareAndSwapLong(this, BASE, cmp, val);
        }
        
        /**
         * CASes the cellsBusy field from 0 to 1 to acquire lock.
         * 通过 CAS 方式获取锁,操作 cellsBusy 的值 0 => 1
         */
        final boolean casCellsBusy() {
            return UNSAFE.compareAndSwapInt(this, CELLSBUSY, 0, 1);
        }
        
        /**
         * Returns the probe value for the current thread.
         * Duplicated from ThreadLocalRandom because of packaging restrictions.
         * 获取当前线程的hash值
         */
        static final int getProbe() {
            return UNSAFE.getInt(Thread.currentThread(), PROBE);
        }
        
        /**
         * Pseudo-randomly advances and records the given probe value for the
         * given thread.
         * Duplicated from ThreadLocalRandom because of packaging restrictions.
         * 重置当前线程的hash值
         */
        static final int advanceProbe(int probe) {
            probe ^= probe << 13; // xorshift
            probe ^= probe >>> 17;
            probe ^= probe << 5;
            UNSAFE.putInt(Thread.currentThread(), PROBE, probe);
            return probe;
        }
    }
    

Striped64#longAccumulate方法

在这个方法中,我们就提到了之前的 LongBinaryOperator 函数,实际在进行计算的时候,是判断是否有这个值来进行处理的。

但我们通过 LongAdder 传过来的 LongBinaryOperator 这个值为 null,所以这里就不用考虑了。

  • Striped64#longAccumulate:cell 数组创建

    abstract class Striped64 extends Number {
        /**
         * Handles cases of updates involving initialization, resizing,
         * creating new Cells, and/or contention. See above for
         * explanation. This method suffers the usual non-modularity
         * problems of optimistic retry code, relying on rechecked sets of
         * reads.
         *
         * @param x the value 期望的值
         * @param fn the update function, or null for add (this convention
         * avoids the need for an extra field or function in LongAdder).
         * 执行更新的方法
         * @param wasUncontended false if CAS failed before call // Cells 初始化之后,并当前线程CAS修改失败 为false
         *
         * 如何进来这个方法
         * 1.cells未初始化并且casBase失败(多线程写base发生竞争了)[初始化cells] -> 进入CASE2 拿到锁了 则创建一个size为2的cells 没拿到锁 数据累加到base中
         * 2.cells的对应的值为null,第一次写值,需要 longAccumulate 支持 -> 进入CASE1.1 创建一个Cell 赋值到对应的下标中
         * 3.cells初始化并且当前线程cas失败,当前线程对应的cell有竞争[重试|扩容] -> 进入CASE1.2 因为之前发生过竞争了 所以需要将wasUncontended重新设置为true 重置当前hash值 再进入CASE1.1 判断依旧不为空 则进入CASE1.3 重试CAS 成功则退出 失败则进入CASE1.5把扩容意向改为true 再rehash 进行自旋 如果CASE1.3依旧CAS失败 则进行CASE1.6扩容逻辑 所以如果执行到扩容 则至少需要3次CAS失败
         */
        final void longAccumulate(long x, LongBinaryOperator fn,
                                  boolean wasUncontended) {
            // h: 线程hash值
            int h;
            // 还没有给线程分配hash值,这个时候先分配一手hash值
            if ((h = getProbe()) == 0) {
                // 等于0 强制设置为当前线程,给当前线程分配 hash 值
                ThreadLocalRandom.current(); // force initialization 强制
                // 重新获取 probe 值,hash值被设置为一个全新的线程,所以设置了 wasUncontended 为true
                h = getProbe();
                // 重新计算了,认为不是热点 也即是说,线程第一次进来,计算的hash值为0,当前线程的hash值0与任何数据位运算都是0,
                // 那么就会落到第一个cell,这是不合理的,所以需要将wasUncontended设置为true
                // 也就是说,首次线程访问,先会写cell[0]号位置
                // 写成功了,就没有下文了,写失败了,说明发生了竞争,那么就不适合再给线程设置为0了,因为已经发生了抢夺
                // 不把它当做一次真正的竞争
                wasUncontended = true;
            }
            // collide 表示扩容意向,false 一定不会扩容,true可能扩容
            boolean collide = false;                // True if last slot nonempty
            // 自旋
            for (;;) {
                // as cells数组引用
                // a 当前线程的命中的cell
                // n cells数组长度
                // v 表示期望值
                Cell[] as; Cell a; int n; long v;
                // CASE1:cells 已经初始化了,当前线程应该将数据写入到对应的cell中
                if ((as = cells) != null && (n = as.length) > 0) {
                    // CASE1.1: 如果计算完成之后,计算当前的cell单元为null,说明这个cell没有被使用,则需要新建 new cell
                    if ((a = as[(n - 1) & h]) == null) {
                        // cellsBusy == 0 锁未被占用
                        // 如果cells数组没有再扩容
                        if (cellsBusy == 0) {       // Try to attach new Cell
                            // 拿当前的x创建一个cell单元
                            Cell r = new Cell(x);   // Optimistically create
                            // 尝试加锁
                            if (cellsBusy == 0 && casCellsBusy()) {
                                // 是否创建成功的标记
                                boolean created = false;
                                // 在有锁的情况下在检查一下之前的判断
                                try {               // Recheck under lock
                                    // rs 表示当前cells的引用
                                    // m 表示cells长度
                                    // j 表示当前线程命中的下标
                                    Cell[] rs; int m, j;
                                    // 条件1和条件2恒成立,因为进来的时候就已经判断了
                                    // 这里大家可能会有疑惑,为毛没有再判断一次 cells == as
                                    // 因为这个时候,两个线程A、B进来之后,A停在加锁之前,B进来了,那么A、B必然插入的是同一个槽
                                    // 这时会判断 rs[j = (m - 1) & h] == null) 为了防止其它线程初始化过该位置 如果为null才赋值 否则就不管了
                                    if ((rs = cells) != null &&
                                        (m = rs.length) > 0 &&
                                        rs[j = (m - 1) & h] == null) { // 所以不加这个判断可能会产生重赋值 造成丢失数据
                                        rs[j] = r;
                                        created = true;
                                    }
                                } finally {
                                    // 解锁
                                    cellsBusy = 0;
                                }
                                // 创建并且赋值成功,跳出循环
                                if (created)
                                    break;
                                // 这个时候目标槽已经不是空了 继续自旋
                                // LongAdder 必须保证每个值都写进去
                                continue;           // Slot is now non-empty
                            }
                        }
                        // 扩容意向修改为false
                        // 因为当前线程还没写值,不一定写失败,所以扩容意向为false
                        collide = false;
                    }
                    // CASE1.2:只有一种情况会进来,cells初始化之后,当前线程竞争修改失败,并且已经初始化线程hash值
                    // wasUncontended目前false,这里是重新设置这个值为 true
                    // 紧接着执行 advanceProbe(h) 重置当前线程的hash,继续循环
                    else if (!wasUncontended) 		// CAS already known to fail
                        wasUncontended = true; 		// Continue after rehash
                   // 如果为true。就继续执行当前cell位的cas操作
                   // CASE1.3:当前线程rehash过hash值,新命中的cell不为空,就来到这里,然后进行cas写
                   // 如果写成功了,跳出循环
                   // 如果写失败了,表示rehash之后,又写失败了,进入下一个条件CASE1.4
                    else if (a.cas(v = a.value, ((fn == null) ? v + x :
                            fn.applyAsLong(v, x))))
                        break;
                    // CASE1.4:
                    // 条件1:n 是数组长度 大于CPU核心数,扩容意向collide设置为false,表示不再扩容了
                    // 条件2:cells != as 其他线程已经扩容过了,当前线程rehash之后重试即可
                    else if (n >= NCPU || cells != as)
                        // 如果n大于等于 NCPU 不可扩容
                        collide = false;            // At max size or stale
                    // CASE1.5:
                    // 设置扩容意向为true,但是不一定扩容成功
                    // 如果扩容意向为false,重新设置为true 然后继续执行循环
                    // 设置为true之后,再次rehash然后再次循环,如果又失败了,这里且不满足 (n >= NCPU || cells != as)
                    // 此时collide为true 执行 CASE1.6
                    else if (!collide)
                        collide = true;
                    // CASE1.6:扩容操作并加锁 注意:只是扩容,并没有进行赋值
                    // 条件1:cellsBusy == 0 表示当前没有占用锁,当前线程可以抢锁
                    // 条件2:casCellsBusy() 抢锁
                    else if (cellsBusy == 0 && casCellsBusy()) {
                        try {
                            // 和上面一样,需要判断,防止重复扩容
                            if (cells == as) { 		// Expand table unless stale
                                // 扩容2倍
                                Cell[] rs = new Cell[n << 1];
                                for (int i = 0; i < n; ++i)
                                    rs[i] = as[i];
                                // 扩容赋值
                                cells = rs;
                            }
                        } finally {
                            // 释放锁
                            cellsBusy = 0;
                        }
                        // 扩容意向设置为false
                        collide = false;
                        continue;           		// Retry with expanded table
                    }
                    // 重新设置线程hash值 h
                    h = advanceProbe(h);
                }
                // CASE2: cells 没加锁并且没有初始化(cells为null),则尝试进行加锁并且开始初始化操作
                // 条件1:cellsBusy == 0 没有加锁
                // 条件2:cells == as 为什么又对比一次?因为多线程场景下,A线程过来,发现是null,然后进入了,然后B线程过来,不一定是null
                // 可能其他线程会修改了cells
                // 条件3:true->表示cas获取锁成功,将cellsBusy设置为1,false->表示其他线程正在持有锁
                else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
                    // 初始化操作
                    boolean init = false;
                    try { 						// Initialize table
                        // 初始化的时候,进行创建cells数组
                        // casBase 失败,新建大小为2的cells数组
                        // 为毛又对比一下?因为是这样的,有可能在执行 cellsBusy == 0 && cells == as && casCellsBusy() 的时候
                        // 执行到 cellsBusy == 0 && cells == as 线程让出CPU 然后另外一个线程执行为true,那么其可以继续操作cells
                        // 此时在切换过来,这个cells就不是null了
                        // 防止其它线程已经初始化了,当前线程再次初始化 导致丢失数据
                        // 类似双重检查锁 - 这里是安全的,因为多线程场景下只有一个线程能够拿到锁,使用casCellsBusy()保证
                        if (cells == as) {
                            // 新建大小为2的cells数组
                            Cell[] rs = new Cell[2];
                            // 设置值
                            rs[h & 1] = new Cell(x);
                            // 赋值cells
                            cells = rs;
                            // 初始化成功
                            init = true;
                        }
                    } finally {
                        cellsBusy = 0; // 最后要释放锁
                    }
                    if (init)
                        break; // 初始化成功则退出自旋
                }
                // CASE3:cells 正在初始化 则尝试在base上进行累加
                // 1.当前cellsBusy被别的线程持有(上锁),表示其它线程正在初始化cells,这个时候需要进行兜底操作,将当前线程值累加到base
                // 2.cells被其他线程初始化后,需当前线程需要将数据要累加到base
                else if (casBase(v = base, ((fn == null) ? v + x :
                        fn.applyAsLong(v, x))))
                    break;              // Fall back on using base
            }
        }
    }
    

    流程图

    JDK8 新特性 LongAdder 源码解析_第7张图片

    JDK8 新特性 LongAdder 源码解析_第8张图片

    每个线程刚进入 longAccumulate 时,会尝试对应一个 cell 对象(找到一个坑位)

    JDK8 新特性 LongAdder 源码解析_第9张图片

LongAdder其他方法

  • 当没有 base 的更新没有线程竞争的时候,会直接写到 base 里面去,而不会操作 Cell 数组,当 base 的写出现了竞争的时候,就会创建 Cell 数组,由不同的线程写不同的下标。当最后求和的时候,通过上述的公式,sum = base + 所有槽的值。获取最终结果通过 sum 整合。

    s u m = b a s e + ∑ i = 0 n C e l l [ i ] sum=base+\sum_{i=0}^{n} Cell[i] sum=base+i=0nCell[i]

    保证最终一致性,不保证强一致性。

    public class LongAdder extends Striped64 implements Serializable {
        
        // 求和方法,只保证最终一致性
        // 计算方式如上
        public long sum() {
            Cell[] as = cells; Cell a;
            long sum = base;
            if (as != null) {
                for (int i = 0; i < as.length; ++i) {
                    if ((a = as[i]) != null)
                        sum += a.value;
                }
            }
            // 不保证返回精确值,他是最终一致性的
            return sum;
        }
    
        // 数值归零方法
        public void reset() {
            Cell[] as = cells; Cell a;
            base = 0L;
            if (as != null) {
                for (int i = 0; i < as.length; ++i) {
                    if ((a = as[i]) != null)
                        a.value = 0L;
                }
            }
        }
    
        // 数值归零并返回sum值
        public long sumThenReset() {
            Cell[] as = cells; Cell a;
            long sum = base;
            base = 0L;
            if (as != null) {
                for (int i = 0; i < as.length; ++i) {
                    if ((a = as[i]) != null) {
                        sum += a.value;
                        a.value = 0L;
                    }
                }
            }
            return sum;
        } 
    }
    

总结

AtomicLong可以弃用了吗

  • 看上去 LongAdder 的性能全面超越了 AtomicLong,而且阿里巴巴开发手册也提及到 推荐使用 LongAdder 对象,比 AtomicLong 性能更好(减少乐观锁的重试次数),但是我们真的就可以舍弃掉 LongAdder 了吗?
  • 当然不是,我们需要看场景来使用,如果是并发不太高的系统,使用 AtomicLong 可能会更好一些,而且内存需求也会小一些。
  • 我们看过 sum() 方法后可以知道 LongAdder 在统计的时候如果有并发更新,可能导致统计的数据有误差 (保证最终一致性,不保证强一致性)
  • 而在高并发统计计数的场景下,才更适合使用 LongAdder。

收获

  • LongAdder 中最核心的思想就是利用空间来换时间,将热点 value 分散成一个 Cell 列表来承接并发的 CAS,以此来提升性能。
  • LongAdder 的原理及实现都很简单,但其设计的思想值得我们品味和学习。



参考

  • 视频参考
    • b站_小刘讲源码公开课 JDK8 新特性LongAdder源码深度讲解,保证让你学到很多硬核知识!
    • b站_黑马程序员深入学习Java并发编程,JUC并发编程全套教程
  • 文章参考
    • 肆华_LongAdder阅读理解
    • shstart _高并发计数器之LongAdder源码解析

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