Java8 源码阅读 - LongAdder

LongAdder和AtomicLong类似是用于多线程下来保证数据更新的原子性,AtomicLong主要是依赖CAS操作来保证原子性的,其方法本质是在循环中一直尝试CAS,直到成功时才退出循环,所以在线程竞争激烈的场景往往性能不是很好(尽管已经比使用悲观锁好的多);

LongAdder采用的是类似分治的思想,再遇到多个线程同时对数据进行更新时,会将数据分为多份更细粒度的子单位再更新,从而达到减少线程竞争的目的,额外的消耗是需要更多的空间;在ConcurrentHashMap中也有它的身影,

源码实现

Striped64是一个抽象类,里面的实现是LongAdder操作方法的基础;该类里面维护了一个表只允许原子性操作并且是懒加载的,大小为2的幂次方;

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

AtomicLong类的变体,只支持原始访问和CAS操作,这个就是LongAdder中细粒度的单位;

/** cpu的数目,决定着细分cell的数量 */
static final int NCPU = Runtime.getRuntime().availableProcessors();

/** cells表,当非空时size是2的幂 */
transient volatile Cell[] cells;

/** 基本值,主要在没有竞争时使用,通过CAS更新 */
transient volatile long base;

/** 自旋锁,通过CAS锁定 */
transient volatile int cellsBusy;

/**  可以看到更新BASE时是用cas操作 */
final boolean casBase(long cmp, long val) {
    return UNSAFE.compareAndSwapLong(this, BASE, cmp, val);
}

/** 从0到1的情况表示获取锁 把cellsBusy置0表示释放锁*/
final boolean casCellsBusy() {
    return UNSAFE.compareAndSwapInt(this, CELLSBUSY, 0, 1);
}

base属性就是没有线程冲突时累加的数值,在遇到线程竞争时才进行分治处理;

final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {
        int h;
        if ((h = getProbe()) == 0) {
            // 当h值为0时表示未初始化 
            ThreadLocalRandom.current(); // 强制初始化
            h = getProbe();
            wasUncontended = true;
        }
        //如果最后一个槽非空,则为真,也用于控制扩容,false重试。
        boolean collide = false;  
        for (;;) { //for死循环
            Cell[] as; Cell a; int n; long v;
            if ((as = cells) != null && (n = as.length) > 0) {
                // 表已经初始化
                if ((a = as[(n - 1) & h]) == null) {
                    // 如果所映射到的槽是空的
                    if (cellsBusy == 0) { //判断锁是否被使用
                        // 锁未被使用,乐观地创建并初始化cell。
                        Cell r = new Cell(x);   // 乐观地创建
                        if (cellsBusy == 0 && casCellsBusy()) {
                            // 锁仍然是空闲的、且成功获取到锁
                            boolean created = false;
                            try {     
                                // 在持有锁时再次检查槽是否空闲
                                Cell[] rs; int m, j;
                                if ((rs = cells) != null && //如果cells不为空
                                    (m = rs.length) > 0 &&
                                    rs[j = (m - 1) & h] == null) {
                                    // 所映射的槽仍为空
                                    rs[j] = r;// 关联 cell 到槽
                                    created = true;
                                }
                            } finally {
                                cellsBusy = 0;// 释放锁
                            }
                            if (created)
                                break;// 成功创建cell并关联到槽,退出循环
                            continue;  //走到这表示上面获取到锁时槽被占用了 需要重新循环申请锁
                        }
                    }
                    collide = false;// 锁被占用了,重试
                }
                // 槽被占用了
                else if (!wasUncontended)       // 已知CAS失败
                    wasUncontended = true;      // 在重散列后继续,在当前槽的cell上尝试更新,重装散列表示重新刷新h值
                else if (a.cas(v = a.value, ((fn == null) ? v + x :
                                             fn.applyAsLong(v, x))))
                    break;;
                    // 表达到上限后(最大为CPU核数)就不会再尝试下面if的扩容了,只会重散列,尝试其他槽
                else if (n >= NCPU || cells != as)
                    collide = false;    
                else if (!collide)
                    collide = true;
                    // 如果不存在冲突,则设置为存在冲突
                else if (cellsBusy == 0 && casCellsBusy()) {
                    // 锁空闲且成功获取到锁
                    // 进到这里表示没有足够的槽添加了 需要进行扩容
                    try {
                        if (cells == as) {      //  距上一次检查后表没有被改变,进行扩容
                            Cell[] rs = new Cell[n << 1]; //扩大一倍
                            for (int i = 0; i < n; ++i)
                                rs[i] = as[i];
                            cells = rs;
                        }
                    } finally {
                        cellsBusy = 0;// 释放锁
                    }
                    collide = false;
                    continue;                   // 在扩容后的表上重试
                }
                // 没法获取锁,重散列,尝试其他槽
                h = advanceProbe(h);
            }
            else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
                //未初始化表,在获取锁成功后初始化表
                boolean init = false;
                try {       
                    if (cells == as) {
                        Cell[] rs = new Cell[2]; //初始化大小为2
                        rs[h & 1] = new Cell(x);
                        cells = rs;
                        init = true;
                    }
                } finally {
                    cellsBusy = 0;
                }
                if (init)
                    break;// 成功初始化,已更新,跳出循环
            }
            else if (casBase(v = base, ((fn == null) ? v + x :
                                        fn.applyAsLong(v, x))))
                //表未被初始化,可能正在被初始化,
                //直接尝试cas设置base值,如果本次cas成功则退出循环,否则重新判断
                break;                          
        }
    }

上面这段代码就是LongAdder类处理多线程冲突下的分治操作,总体而言操作流程如下;

  1. 发生线程竞争,判断是否初始化过Table;如未初始化,会创建一个容量为2的Table,并且将value插入其中一个插槽;
  2. 第二次发生线程竞争时,会先根据ThreadLocalRandom的探针计算哈希值来寻找插槽,如果插槽为空,则插入插槽,如果插槽不为空,会尝试通过CAS更新该插槽的值;
  3. 如果插槽不为空且CAS更新失败,则会尝试扩建Table,最多扩张到大于或等于最接近CPU核数的2的幂次方;

通过ThreadLocalRandom的探针字段来用于每个线程的哈希码,为0意味着未初始化,发生线程冲突时,如果Table容量不能再扩大,且CAS操作失败,则会进行双重哈希,使用辅助哈希Marsaglia XorShift尝试查找空闲插槽;

//Marsaglia XorShif随机数算法
static final int advanceProbe(int probe) {
        probe ^= probe << 13;   // xorshift
        probe ^= probe >>> 17;
        probe ^= probe << 5;
        UNSAFE.putInt(Thread.currentThread(), PROBE, probe);
        return probe;
}

回到LongAdder看下add方法

   public void add(long x) {
        Cell[] as; long b, v; int m; Cell a;
        if ((as = cells) != null || !casBase(b = base, b + x)) {
            // cells 不为空 或更新base的cas失败,也即出现了竞争。
            boolean uncontended = true;
            if (as == null || (m = as.length - 1) < 0 || // as 为空表示cells未被初始化 || cells的长度为0
                (a = as[getProbe() & m]) == null || // as[i]上的值为null,表示该位置没有被占用
                !(uncontended = a.cas(v = a.value, v + x))) //cas 成功,将as[i]的值替换成 oldvalue + x
                // 如果所映射的槽不为空,且成功更新则返回,否则进入复杂处理流程。
                longAccumulate(x, null, uncontended);
        }
}

可以看出在没有线程冲突时还是会先通过cas更新base值,极力避免进到Striped64的复杂处理流程;

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聚合操作就是将base值和Table中所有Cell值相加得出,下图可能有点不准确,但是思想是类似的;

你可能感兴趣的:(Java8 源码阅读 - LongAdder)