java并发包之LongAdder源码解析

586 阅读6分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

LongAdder这个类是是jdk1.8开始有的,这个类的主要作用就是用来计数的,并且是线程安全的类,说到计数,那么我们会想到java并发包里有AtomicLong这个原子类,同样可以用来计数,那为什么又加了LongAdder这个类呢,先来看一张图:图片

AtomicLong保证原子性的原理就是通过cas操作,当多个线程去修改base值时,就会发生竞争,通过cas自旋的方式来等待获取修改base的权限,如果在高并发的情况下,多个线程等待获取操作权限,不断自旋,这个自旋操作也会很耗时,这时,LongAdder类就提供了另一个方案来解决这个问题,看这张图,当没有竞争的时候,LongAdder和AtomicLong操作一样,通过cas操作进行对base的操作,当有别的线程也来竞争操作这个值,没有抢到锁的线程就会直接将值放入cells数组中,其他线程也是同样的操作,而不是等待自旋,最后对cells数组的每个桶位的value值和base值相加就得到最终的计数,可以看到这种方式相比AtomicLong就效率高很多,将多个线程等待自旋的操作分散为对数组每个桶位进行操作,减少了竞争。

在并发量不是很大的时候,AtomicLong和LongAdder其实性能相差不是很大,但是当并发量很高的情况下,LongAdder的性能就比AtomicLong高很多。LongAdder在ConCurrentHashMap中也有应用,ConCurrentHashMap在Jdk1.8进行了改动,其计数map中的元素数量就用到了LongAdder保证原子性,ConCurrentHashMap会在下一篇文章中讲到。下面来好好分析下LongAdder源码

01

LongAdder结构

图片

LongAdder会继承Striped64这个类,这个类会定义一些关键属性

    @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;
                valueOffset = UNSAFE.objectFieldOffset
                    (ak.getDeclaredField("value"));
            } catch (Exception e) {
                throw new Error(e);
            }
        }
    }

    /**CPU的数量*/
    static final int NCPU = Runtime.getRuntime().availableProcessors();

    /**
     * 存储各个线程计数的值,每个桶位下标位置是通过线程hash值来计算的
     */
    transient volatile Cell[] cells;

    /**
     * 无竞争时计数的值
     */
    transient volatile long base;

    /**
     * 相当于是获取锁的标识字段
     */
    transient volatile int cellsBusy;

    /**
     * Package-private default constructor
     */
    Striped64() {
    }

    /**
     * 通过cas操作更新base值
     */
    final boolean casBase(long cmp, long val) {
        return UNSAFE.compareAndSwapLong(this, BASE, cmp, val);
    }

    /**
     * 计算是否能获取到锁
     */
    final boolean casCellsBusy() {
        return UNSAFE.compareAndSwapInt(this, CELLSBUSY, 01);
    }

    /**
     * 计算当前线程的hash值
     */
    static final int getProbe() {
        return UNSAFE.getInt(Thread.currentThread(), PROBE);
    }

这些属性现在看可能还摸不着头脑,不急,往下分析源码就知道作用是啥了。

02

add方法

LongAdder的核心方法就是add方法,源码的分析也就通过add方法来展开。

 public void add(long x) {
        // as表示 cell引用
        // b:表示获取的base值
        // v 表示期望值
        //m 表示cells数组的长度-1
        // a 表示当前线程命中的cell单元格
        Cell[] as;
        long b, v;
        int m;
        Cell a;
        // 条件一:true->表示cells已经初始化过了,当前线程应该将数据写入到对应cell中
        // false->表示cells未初始化,当前所有线程都应该将数据写入base中

        // 条件二:true -> 表示当前线程cas替换数据成功。
        // false --> 表示发生了竞争,可能需要重试或者扩容

        if ((as = cells) != null || !casBase(b = base, b + x)) {
          // 什么时候会进来
            // 1.true ->表示cells已经初始化过了,当前线程应该将数据写入到对应cell中
            //2. false --> 表示发生竞争了,可能需要重试或者扩容
            boolean uncontended = true;

            //条件一:true:说明cell未初始化,也就是多线程写base发生竞争了
            //       false: 表示cells已经初始化,当前线程应该是找自己的cell写值

            // 条件二:getProbe() 获取当前线程hash值
            // true:说明当前线程对应下标的cell为空,需要创建longAccumulate支持
            // false:说明当前线程对应的cell不为空,说明下一步想要将x值添加到cell中

            // 条件三:true:表示cas失败,意味着当前线程对应的cell有竞争
            // false --> 表示cas成功
            if (as == null || (m = as.length - 1) < 0 ||
                    (a = as[getProbe() & m]) == null ||
                    !(uncontended = a.cas(v = a.value, v + x)))
                longAccumulate(x, null, uncontended);
        }
    }

add方法大致可以分为两个部分

1 第一个if判断解析

可以看到if有两个判断,满足其中一个就会进入if块,as==cells!=null,如果cells不等于空,说明cells数组已经初始化过了,那么线程往里写值就写到cell数组对应的cell中;

!casBase(b = base, b + x),如果线程操作base不成功,说明遭到其他线程竞争,并且当前线程竞争失败。

2 第二个if判断解析

第二个if就稍微有点复杂,这个if就是判断是否需要调用longAccmulate方法,那么满足什么条件才能调用呢?

  1. 如果当前cells数组还没有初始化

  2. 线程对应cells数组下标的cell为空

  3. 操作当前下标的cell发生了竞争

03

longAccumulate方法

longAccumulate方法应该算是整个LongAdder最核心最复杂的方法了,这个方法主要做了哪些事呢,其实根据上面的if分析大致就可以了解到。

  1. 进行初始化数组

  2. 创建Cell对象

  3. 对Cell扩容

  4. 操作Cell发生竞争则操作base进行值的写入

这四个事不是每个都会发生,是有可能发生,具体怎么触发接着来看longAccumulate方法

图片

这个方法主要就是做这我箭头指的四个操作,我们针对这四个模块逐一分析

1  第一个模块 计算当前线程hash值

线程归属于cells是数组哪个桶位就是根据线程来计算它的hash值来得到,当然也不是绝对的,后面会说到,如果当前桶位存在竞争,可能会进行rehash。

2  第二个模块 new Cell,对数组扩容

首先整个是个for循环,也可以理解为自旋的操作。满足第一个if块的条件就是cells数组已经初始化,并且长度大于0,先来看看这个if块源码

   if ((as = cells) != null && (n = as.length) > 0) {
                // 表示当前线程对应数组下标位置为空,需要new Cell
                if ((a = as[(n - 1) & h]) == null) {
                    // cellsBusy等于0代表未被占用
                    if (cellsBusy == 0) {
                        //
                        Cell r = new Cell(x);   // Optimistically create
                        if (cellsBusy == 0 && casCellsBusy()) {
                            //获取锁成功将创建的cell放入数组对应下标位置
                            boolean created = false;
                            try {               // Recheck under lock
                                Cell[] rs; int m, j;
                                if ((rs = cells) != null &&
                                    (m = rs.length) > 0 &&
                                        //将cell放入数组对应下标位置
                                    rs[j = (m - 1) & h] == null) {
                                    rs[j] = r;
                                    created = true;
                                }
                            } finally {
                                cellsBusy = 0;
                            }
                            if (created)
                                break;
                            continue;           // Slot is now non-empty
                        }
                    }
                    collide = false;
                }
                else if (!wasUncontended)       // CAS already known to fail
                    wasUncontended = true;      // Continue after rehash
                else if (a.cas(v = a.value, ((fn == null) ? v + x :
                                             fn.applyAsLong(v, x))))
                    break;
                    //条件一:n >= NCPU true->扩容意向 改为false,表示不扩容了  false-> 说明cells数组还可以扩容
                    //条件二:cells != as true->其它线程已经扩容过了,当前线程rehash之后重试即可
                else if (n >= NCPU || cells != as)
                    collide = false;            // At max size or stale
                else if (!collide)
                    collide = true;
                //真正扩容的逻辑
                //casCellsBusy为true表示当前线程获取锁成功
                else if (cellsBusy == 0 && casCellsBusy()) {
                    try {
                        if (cells == as) {
                            // 扩容为原来的2倍
                            Cell[] rs = new Cell[n << 1];
                            for (int i = 0; i < n; ++i)
                                rs[i] = as[i];
                            //将扩容后的新数组赋值给cells
                            cells = rs;
                        }
                    } finally {
                        cellsBusy = 0;
                    }
                    collide = false;
                    continue;                   // Retry with expanded table
                }
                // 重新hash
                h = advanceProbe(h);
            }

源码开始有个判断cellsbusy是否等于0,这个cellsbusy一开始我有介绍,作用就是判断是否有别的线程也在这个桶位进行操作,等于0,就代表没有被占用。

  1. new 一个Cell,将值放入cell中

  2. 然后通过casCellsBusy()获取到锁,将cell放入到数组下标桶位

  3. 接下来是判断当前是否需要扩容,扩容的条件就是线程数要小于Cpu的数量,并且当前线程没有竞争到操作cell的锁,那就会执行扩容操作,扩容为原来的两倍

  4. 最后有个重新hash的操作,这个操作会在当前线程操作所在下标位置cell没有获取到锁,就会重新hash寻找下一个位置

3  第三个模块 对cells数组进行初始化

    // cells还没有初始化并且当前线程获取到锁
            else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
                boolean init = false;
                try {
                    // 进行初始化
                    if (cells == as) {
                        Cell[] rs = new Cell[2];
                        rs[h & 1] = new Cell(x);
                        cells = rs;
                        init = true;
                    }
                } finally {
                    cellsBusy = 0;
                }
                if (init)
                    break;
            }

这里就是获取到执行的锁后对数组初始化,并将数据放入到初始化后的数组中去。

4  第四个模块 将值写入base中

       //1.当前cellsBusy加锁状态,表示其它线程正在初始化cells,所以当前线程将值累加到base
            //2.cells被其它线程初始化后,当前线程需要将数据累加到base
            else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
                break;        

如果当前cellsBusy处于加锁状态,就意味着别的线程再初始化cells,那么当前线程就无法将值放入cells数组中,所以就会操作base,将值写入base中。

总结

(1)LongAdder通过base和cells数组来存储值;

(2)不同的线程会hash到不同的cell上去更新,减少了竞争;

(3)LongAdder的性能非常高,最终会达到一种无竞争的状态