LongAdder源码分析

167 阅读6分钟

简介

LongAddr是java8中新增的原子类,在竞争比较激烈的情况下性能要高出AtomicLong很多。

原理

LongAddr在最初无竞争的情况下会把值更新到base属性上,当存在竞争会初始化cells对象,把各个线程的值分段到不同的对象上,也类似于热点分段。

1.png

源码分析

LongAdder继承自Striped64抽象类,Striped64中定义了Cell内部类和各重要属性。

主要内部类

 /**
 *  消除伪共享的内部类
 *
 */
@sun.misc.Contended static final class Cell {
    volatile long value; // volatile修饰,修改之后对其它线程立即可见
    Cell(long x) { value = x; }

    final boolean cas(long cmp, long val) {
        return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val); // 原子更新value的值
    }

    // Unsafe mechanics
    private static final sun.misc.Unsafe UNSAFE; // 魔法类,对这个类不熟悉的可以自行学习
    private static final long valueOffset; // value在Cell对象中的偏移量,一旦类被加载,对应属性的偏移量就固定了
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe(); // 获取魔法类,app类加载器加载的类不能通过这种方式使用
            Class<?> ak = Cell.class;
            valueOffset = UNSAFE.objectFieldOffset // 偏移地址
                (ak.getDeclaredField("value"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }
}

对于伪共享不太了解的也可以看这里:juejin.cn/post/694026…

主要属性

/**
 * cells数组,存储热点对象Cell,在没有竞争的情况下是null
 * 一旦存在竞争就会初始化cells数组,初始容量为2,最大不超过cpu的数量
 * cells一旦初始化,就算后面没有竞争,线程更新值也是更新到Cell对象上,不会在更新到base属性上
 */
transient volatile Cell[] cells;

// 在没有竞争的情况下,值会更新此属性上
transient volatile long base;

// 乐观锁,当修改cells数组会使用此变量
transient volatile int cellsBusy;

add(long x)

public void add(long x) {
    Cell[] as; long b, v; int m; Cell a;
    // 1. 判断cells有没有初始化,如果base没有竞争,cells不会初始化
    // 2. 如果cells是null且通过cas更新base成功,直接返回,否则有竞争,进入if语句内,初始化cells
    if ((as = cells) != null || !casBase(b = base, b + x)) {
        boolean uncontended = true; // 没有竞争
        /**
         * 1. as是null,成立则进入if语句块,执行初始化cells操作
         * 2. as的数组长度是0
         * 3. 如果cells被初始化,且它的长度不为0,则通过getProbe方法获取当前线程threadLocalRandomProbe变量的值,初始为0,
         * 然后执行threadLocalRandomProbe&(cells.length-1 ),
         * 相当于m%cells.length;如果cells[threadLocalRandomProbe%cells.length]的位置为null,
         * 这说明这个位置从来没有线程做过累加,需要进入if继续执行,在这个位置创建一个新的Cell对象
         * 4. 当前线程在cells能够匹配到cell对象,并且做过累加操作,如果cas更新失败,说明此cell存在竞争,uncontended为false
         */
        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); // 调用父类方法,第一个参数是当前累加的值,第二个参数是当前cell中否存在竞争
    }
}
  1. 不存在竞争者的情况下值只更新到base属性上
  2. 一旦有竞争就会初始化cells对象
  3. 如果当前线程hash到位置有竞争,就可能需要扩容了

longAccumulate(long x, LongBinaryOperator fn,boolean wasUncontended)

final void longAccumulate(long x, LongBinaryOperator fn,
                              boolean wasUncontended) {
    int h;
    if ((h = getProbe()) == 0) { // 获取当前线程的threadLocalRandomProbe作为hash,如果是0证明当前线程还没有竞争过cell
        // 初始化当前线程的threadLocalRandomProbe
        ThreadLocalRandom.current(); // force initialization
        h = getProbe(); // 重新获取threadLocalRandomProbe
        wasUncontended = true; // 不存在竞争
    }

    // collide 翻译过来的意思是否有碰撞,也可以理解为是否有扩容的意向,true是有意向扩容
    boolean collide = false;                // True if last slot nonempty
    /**
     * 先大概描述下for循环内所做的工作:
     *  if ((as = cells) != null && (n = as.length) > 0) {}
     *  else if (cellsBusy == 0 && cells == as && casCellsBusy()) {}
     *  else if (casBase(v = base, ((fn == null) ? v + x :
     *                                         fn.applyAsLong(v, x))))
        最外层有三个判断条件
        条件1. cells已经初始化且长度大于0,如果不成立说明cells还没有初始化,执行条件2进行初始化工作
        条件2. cellsBusy没有被占用,cells是null,且cas cellsBusy成功,如果有一个失败说明有其它线程正在初始化cells,执行条件3
        条件3. 有其它线程正在初始化cells,先把数值累加到base属性上
     整体来看结构是非常清晰的,下面进入具体的分支分析。
     */
    for (;;) { // 自旋
        Cell[] as; Cell a; int n; long v;
        if ((as = cells) != null && (n = as.length) > 0) { // cells已经初始化
            /**
             * 内部分支1:当前线程hash到的位置内容为null,说明没有竞争,创建对应位置的cell对象
             */
            if ((a = as[(n - 1) & h]) == null) {
                if (cellsBusy == 0) {  // 没有加锁      // Try to attach new Cell
                    Cell r = new Cell(x); // 创建Cell  // Optimistically create
                    if (cellsBusy == 0 && casCellsBusy()) { // 再次判断cellsBusy,且如果加锁成功执行以下逻辑
                        boolean created = false; // 创建Cell的状态
                        try {               // Recheck under lock
                            Cell[] rs; int m, j;
                            // 再次判断cells不为null且长度大于0 且hash到位置的Cell为null
                            if ((rs = cells) != null &&
                                (m = rs.length) > 0 &&
                                rs[j = (m - 1) & h] == null) {
                                rs[j] = r; // 把Cell放到hash到位置上
                                created = true; // 创建成功
                            }
                        } finally {
                            cellsBusy = 0; // 释放锁
                        }
                        if (created)
                            break;
                        continue;   // 执行到这里说明获取锁之后有其它线程已经hash到对应位置上了,存储竞争,重新循环        // Slot is now non-empty
                    }
                }
                collide = false; // 没有扩容意向
            }
            /**
             * 内部分支2:!wasUncontended 存在竞争,执行这个分支说明在子类尝试做过累加操作,但失败了
             *  wasUncontended = true; // 在这里改成不存在竞争,下次循环在尝试cas一次
             */
            else if (!wasUncontended)     // CAS already known to fail
                wasUncontended = true;  // Continue after rehash
            /**
             * 内部分支3:cas做累加操作,如果累加成功则退出,累加失败说明当前Cell存在竞争,
             * for循环的最低处会重新计算当前线程的threadLocalRandomProbe
             */
            else if (a.cas(v = a.value, ((fn == null) ? v + x :
                                         fn.applyAsLong(v, x))))
                break;
            /**
             * 内部分支4:如果Cell对象的个数大于等于cpu个数或者其它线程已经做过扩容
             * collide = false; 扩容意向改成false
             * 在这里特别说明一下:如果cells的容量已经达到了cpu的个数,下面两个内部分支将永远不会执行了,也就是不会在扩容了
             * 只能每次重新计算下当前线程的threadLocalRandomProbe(有可能新hash到位置就是null了或者hash到Cell对象竞争不激烈了,在内部分支3 cas成功了就退出了)
             */
            else if (n >= NCPU || cells != as)
                collide = false;            // At max size or stale
            /**
             * 内部分支5: 执行到这里如果collide=false,说明需要扩容了
             * collide = true; 扩容意向改成有扩容意向,下次循环就要开始扩容了
             */
            else if (!collide)
                collide = true;
            /**
             * 内部分支6:如果没有其它线程加锁且cas加锁成功
             */
            else if (cellsBusy == 0 && casCellsBusy()) {
                try {
                    if (cells == as) { // 检查cells有没有变化,有可能在加锁成功之后其它线程已经改了cells
                        Cell[] rs = new Cell[n << 1]; // 扩容为原来的2倍
                        for (int i = 0; i < n; ++i)
                            rs[i] = as[i]; // 对应位置的cell转移到新的数组上
                        cells = rs; // cells指向新的数组
                    }
                } finally {
                    cellsBusy = 0; // 释放锁
                }
                collide = false; // 没有扩容意向
                continue;  // 开始下次循环
            }
            h = advanceProbe(h); // 重新计算线程的threadLocalRandomProbe
        }
        /**
         * 外部分支2:cells数组还没有初始化, 如果cellsBusy是0且cells没有改变且cas加锁成功就准备初始化cells数组了
         */
        else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
            boolean init = false; // 初始化数组状态
            try {                           // Initialize table
                if (cells == as) { // 再次判断cells指向有没有变化,有可能在当前线程cas锁成功之后,其它线程刚初始化了cells数组并释放了锁
                    Cell[] rs = new Cell[2]; // 初始化容量为2
                    rs[h & 1] = new Cell(x); // 占用一个位置
                    cells = rs; // cells指向新创建的数组对象
                    init = true; // 标识创建数组成功
                }
            } finally {
                cellsBusy = 0; // 释放锁
            }
            if (init)
                break; // 退出循环
        }
        /**
         * 外部分支3:如果cells数组没有初始化且其它线程正在初始化,则当先线程的值先累加到base属性上
         */
        else if (casBase(v = base, ((fn == null) ? v + x :
                                    fn.applyAsLong(v, x))))
            break;                          // Fall back on using base
    }
}
  1. cells已经初始化且长度大于0
    1.1. 当前线程hash到的位置没有Cell对象,则尝试在此位置上创建Cell对象,成功放入此位置就返回,否则重试
    1.2. 当前线程对应的Cell对象存在竞争
    1.3. 尝试把值累加到对应的Cell对象上
    1.4. 如果cells的容量大于cpu个数或者有其它线程已经扩容了
    1.5. 有扩容意向
    1.6. 扩用cells数组为之前的2倍

  2. cells还未初始化且没有其它线程在初始化

  3. cells正在初始化(其它线程)就把当前的值更新到base属性上

sum()

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;
}

sum方法相对来说还是比较简单的,就不多做介绍了,简单说一下:

  1. 如果cells不为null,说明cells数组已经初始化了,需要把Cell和base累加
  2. 如果调用sum的同时,有其它线程在操作某个段,有可能值不是最新的 参考: www.cnblogs.com/tong-yuan/p…