07.LongAdder

81 阅读8分钟

LongAdder

1.介绍

LongAdder是JDK8新增加的一个类,主要功能与AtomicLong一样都是做整型自增的功能,但是AtomicLong在高并发情况下性能较差所以才会有LongAdder

2.阿里巴巴开发手册的说明

20210711111923309.png

3.AtomicLong为什么在高并发情况下性能差

AtomicLong内部使用CAS+自旋的方式完成工作,在高并发环境下,N个线程同时进行自旋操作,会出现大量失败并不断自旋的情况,此时AtomicLong的过多的自旋失败会导致性能成为瓶颈

20210711112333483.png

4.LongAdder与AtomicLong的性能测试

代码

public class TestLongAdder {
    public static void main(String[] args) throws Exception {
        testAtomicLongAdder(1, 10000000);
        testAtomicLongAdder(10, 10000000);
        testAtomicLongAdder(100, 10000000);
    }

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

结果

2021071111330083.png

随着并发的增加,AtomicLong的性能急剧下降,耗时是LongAdder的数倍

5.为什么这么快

分段累加

在AtomicLong中所有的线程都是对AtomicLong中的value进行自增的这样会导致CAS失败次数较多,LongAdder的核心思想就是减少CAS失败次数,通过分段加锁的方式实现了减少CAS失败次数

1749703723507.png

20210711113719961.png

消除伪共享

在LongAdder的父类Striped64中存在一个Cell数组,其长度是2的幂次,每个Cell都使用@Contended注解进行修饰,而 @Contended注解可以进行缓存行填充,从而解决缓存行伪共享的问题。伪共享会导致缓存行失效,缓存一致性开销变大

@sun.misc.Contended static final class Cell {

}

6.继承结构

1749703842584.png

7.LongAdder源码解析

介绍

LongAdder里面方法不少,但是咱们只看一个核心入口方法add和求和方法sum即可,其他核心逻辑都在父类里面

sum方法

    public long sum() {
    	// 获得Cell数组
        Cell[] as = cells; Cell a;
        // 获得base
        long sum = base;
        // 如果as不是NULL说明发生了竞争需要对Cell数组求和
        if (as != null) {
        	// 遍历Cell数组
            for (int i = 0; i < as.length; ++i) {
            	// 获得当前Cell并不是NULL
                if ((a = as[i]) != null)
                	// 累加sum
                    sum += a.value;
            }
        }
        // 如果as是NULL则直接返回base,如果不是NULL则返回base+Cell数组的和
        return sum;
    }

add方法

    public void add(long x) {
        /*
         * as:Cell数组
         * b:表示获取的base值
         * v:表示期望值
         * m:表示celss数组的长度
         * a:表示当前线程命中的cell单元格
         */ 
        Cell[] as; long b, v; int m; Cell a;
        /*
         * (as = cells) != null:Cell不是NULL说明已经有竞争了,Cell数组已经初始化
         * !casBase(b = base, b + x):多线程修改base变量了,发生了竞争导致失败
         *
         */
        if ((as = cells) != null || !casBase(b = base, b + x)) {
        	// 
            boolean uncontended = true;
            /*
             * as == null:说明是因为多线程修改base失败进来的
             * (m = as.length - 1) < 0:Cell数组有并且长度不是0
             * (a = as[getProbe() & m]) == null:对Cell数组取余找到当前线程的Cell下标,而且已经初始化对应Cell
             * !(uncontended = a.cas(v = a.value, v + x)):修改当前线程对应的Cell的value如果失败则进入longAccumulate
             */
            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. 判断cells数组是否初始化,如果没有初始化则累加到base上
  2. 如果celss数组没有初始化,并且竞争base失败,则准备进入longAccumulate方法
  3. 如果cells数组已经初始化,并且当前线程对应的cell对象没有初始化,则进入longAccumulate方法
  4. 如果cells数组已经初始化,并且当前线程对应的cell对象已存在,那么尝试使用CAS修改对应cell对象的value,如果存在竞争修改失败,则进入longAccumulate方法

8.Striped64源码解析

静态内部类

	// 静态内部类分段锁的操作对象,使用Contended注解解决缓存行伪共享的问题
    @sun.misc.Contended static final class Cell {
    	// 自增属性
        volatile long value;
        
        // 构造方法
        Cell(long x) { value = x; }
        
        // 封装的CAS操作
        final boolean cas(long cmp, long val) {
            return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
        }

        // Unsafe实例
        private static final sun.misc.Unsafe UNSAFE;
        // value属性的内存偏移地址
        private static final long valueOffset;
        // 初始化Unsafe类和重要属性的内存偏移
        static {
            try {
            	// 通过Unsafe提供的方法获得Unsafe实例
                UNSAFE = sun.misc.Unsafe.getUnsafe();
                // 获取value的内存偏移量
                Class<?> ak = Cell.class;
                valueOffset = UNSAFE.objectFieldOffset
                    (ak.getDeclaredField("value"));
            } catch (Exception e) {
                throw new Error(e);
            }
        }
    }

核心属性

	/**
	 * 获取系统的逻辑核数量,这个值控制着Cell对象的数量,一旦Cell对象数量大于了逻辑核数量那么会造成上下文切换频繁
	 */
    static final int NCPU = Runtime.getRuntime().availableProcessors();

    /**
     * Cell数组,长度是2的N次幂
     */
    transient volatile Cell[] cells;

    /**
     * 未发生竞争时的累加单元
     */
    transient volatile long base;
    
    /**
     * 初始化cells或者扩容cells需要获取锁, 0:表示无锁状态 1:表示其他线程已经持有了锁,类似于AQS的state
	 */
    transient volatile int cellsBusy;

辅助方法

    /**
     * 封装了对于base的CAS操作
     */ 
    final boolean casBase(long cmp, long val) {
        return UNSAFE.compareAndSwapLong(this, BASE, cmp, val);
    }

    
    /**
     * 封装了对于cellsBusy的CAS操作
     */ 
    final boolean casCellsBusy() {
        return UNSAFE.compareAndSwapInt(this, CELLSBUSY, 0, 1);
    }
    

    /**
     * 封装了对于probe的CAS操作
     */ 
    static final int getProbe() {
        return UNSAFE.getInt(Thread.currentThread(), PROBE);
    }
    
    /**
     * 重新设置线程的Prob
     */ 
    static final int advanceProbe(int probe) {
        probe ^= probe << 13;   
        probe ^= probe >>> 17;
        probe ^= probe << 5;
        UNSAFE.putInt(Thread.currentThread(), PROBE, probe);
        return probe;
    }

初始化Unsafe和其他偏移量的代码

    /**
     * Unsafe实例
     */
    private static final sun.misc.Unsafe UNSAFE;
    /**
     * 各个属性内存偏移量
     */
    private static final long BASE;
    private static final long CELLSBUSY;
    private static final long PROBE;
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> sk = Striped64.class;
            BASE = UNSAFE.objectFieldOffset
                (sk.getDeclaredField("base"));
            CELLSBUSY = UNSAFE.objectFieldOffset
                (sk.getDeclaredField("cellsBusy"));
            Class<?> tk = Thread.class;
            PROBE = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomProbe"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }

longAccumulate方法

有哪些情况会进入这个方法
  • cells数组没有初始化并且CAS累加base失败
  • cells数组已经初始化,但是当前线程对应的cell对象为空
  • cells数组已经初始化,当前线程对应cell对象也不为null,但是对对应的cell对象进行CAS累加失败。
方法入参
  • long x:需要增加的值,一般默认都是1
  • LongBinaryOperator fn:默认传递的是null
  • wasUncontended:竞争标识,如果是false则代表有竞争。只有cells初始化之后,并且当前线程CAS竞争修改指定cell对象失败,才会是false
方法的作用
  • 初始化Celss数组并找到当前线程对应的Cell对象并累加
  • 初始化当前线程对应的Cell对象并累加
  • 对指定Cell单元的累加操作进行重试
源码
    final void longAccumulate(long x, LongBinaryOperator fn,
                              boolean wasUncontended) {
        // 存储当前线程的probe
        int h;
        // 如果是0则表示没有初始化
        if ((h = getProbe()) == 0) {
        	// 获取一个值
            ThreadLocalRandom.current(); 
            // 再次获取probe给h
            h = getProbe();
            wasUncontended = true;
        }
        // 用于标记是否发生冲突
        boolean collide = false; 
        // 自旋
        for (;;) {
            Cell[] as; Cell a; int n; long v;
            /*
             * (as = cells) != null:Cell数组已经初始化
             * (n = as.length) > 0:Cell数组长度大于0
             * 总的来说就是判断Cell已经初始化并且有长度
             */
            if ((as = cells) != null && (n = as.length) > 0) {
            	// 获取对应的Cell,如果为空,则说明没有初始化
                if ((a = as[(n - 1) & h]) == null) {
                	// 表示当前没有其他线程创建或扩容Cell数组,也没有线程创建Cell
                    if (cellsBusy == 0) {       
                    	// 创建一个Cell
                        Cell r = new Cell(x);
                        // 如果还是0则尝试CAS修改cellsBusy从0->1
                        if (cellsBusy == 0 && casCellsBusy()) {
                        	// 是否创建成功,默认为false不成功
                            boolean created = false;
                            try {              
                                Cell[] rs; int m, j;
                                // 重新获取Cell数组然后检查然后把Cell初始化进去
                                if ((rs = cells) != null &&
                                    (m = rs.length) > 0 &&
                                    rs[j = (m - 1) & h] == null) {
                                    rs[j] = r;
                                    created = true;
                                }
                            } finally {
                            	// 表示当前没有其他线程创建或扩容Cell数组,也没有线程创建Cell
                                cellsBusy = 0;
                            }
                            // 创建成功就break跳出循环,因为Cell里面装的已经是要累加的值了直接结束逻辑即可
                            if (created)
                                break;
                            // 否则继续循环    
                            continue;           
                        }
                    }
                    // CAS修改cellsBusy为1失败就会来到这里
                    collide = false;
                }
                // 走到这个条件说明当前线程对应的cell对象不为空,如果这个条件成立说明是因为有竞争才进入longAccumulate方法的,然后会去重置竞争标记为true也就是没有竞争,因为自旋后面回rhash
                else if (!wasUncontended)    
                    wasUncontended = true;
                // 再次尝试CAS更新当前线程所在Cell的值,如果成功了就返回
                else if (a.cas(v = a.value, ((fn == null) ? v + x :
                                             fn.applyAsLong(v, x))))
                    break;
                // 如果Cell数组的长度达到了CPU核心数,或者Cell数组扩容了
                else if (n >= NCPU || cells != as)
                    // 设置false并通过下面的语句修改线程的probe再重新尝试
                    collide = false;            
                else if (!collide)
                    collide = true;
                // 如果cellsBusy == 0并且CAS设置为1成功,则尝试扩容Cell数组
                else if (cellsBusy == 0 && casCellsBusy()) {
                    try {
                        // 如果还是那个Cell
                        if (cells == as) {
                            // 扩容
                            Cell[] rs = new Cell[n << 1];
                            for (int i = 0; i < n; ++i)
                                rs[i] = as[i];
                            cells = rs;
                        }
                    } finally {
                        // 重置标记为0
                        cellsBusy = 0;
                    }
                    // 已解决冲突
                    collide = false;
                    // 扩容完成后重新尝试
                    continue;                   
                }
                // 重新计算h值
                h = advanceProbe(h);
            }
            // 如果Cell数组没有初始化则尝试初始化
            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;
            }
            // 如果有其他线程在初始化Cell数组中,就尝试更新base,如果成功就返回
            else if (casBase(v = base, ((fn == null) ? v + x :
                                        fn.applyAsLong(v, x))))
                break;                         
        }
    }