从AtomicLong 到 LongAdder

AtomicLong性能瓶颈分析

AtomicLong关键代码他用得是Unsafe的代码:

public final long getAndAddLong(Object var1, long var2, long var4) {
        long var6;
        do {
            var6 = this.getLongVolatile(var1, var2);
        } while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));

        return var6;
    }

思考这段代码或者说cas存在什么问题,cas的愿意就是底层lock前置使得cpu的指令具有原子性,在多个线程进行同时修改只能一个线程能够成功, 那么我们就假设有16个线程(为啥16个呃,一般16核所以同时能够运行16个线程),那么15个线程就需要失败 ,所以我们可以粗略的估计一下 16个线程 要循环几次才能累加16次, 161514.。。。。, 这个叫啥数学上的排列组合吗? 那么我们的cpu是不是白白的浪费了,多个线程一直在空转。

造成整个问题的原因在于?
呃,因为累加的地址空间值只有一位,那么我们想个方法把累加计数的地址空间变成多份,最后在统计结果时候把多个地址空间累加起来,就可以得到总的计数。

好的,那么问题来了,是不是这个地址空间越多越好呢?
依据就是根据cpu的核数, 因为cas最好的情况的就是不要发生空转,也就是cas竞争失败,所以一个地址变量最好一个线程来运行,那就说明一个地址变量代表一个线程, 我们知道同时指向的线程数是和cpu的核数对应的,所以最好的情况就是地址空间开辟为核数的个数,但是如果线程并发个数很少,就只有一个?需要一开始就开辟16个地址空间吗,所以。。。,得有个升级过程,比如cas失败那就去开辟空间。。。

所以AtomicLong的缺点就是 不能有效利用cpu核数, 线程多了之后会导致cpu空转,耗费cpu资源。

案例证明

public class TestLongAddr {
    public static void main(String[] args) throws InterruptedException {
        /*线程个数*/
        int threadCount = 100;
        /*每个线程的累加个数*/
        int count = 1000000;

        AtomicLong atomicLong = new AtomicLong();
        LongAdder longAdder = new LongAdder();

        CountDownLatch countDownLatch = new CountDownLatch(threadCount);
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < threadCount; i++) {
            new Thread(()->{
                for (int i1 = 0; i1 < count; i1++) {
                    atomicLong.addAndGet(1);
                }
                countDownLatch.countDown();
            }).start();
        }
        countDownLatch.await();
        System.out.println(atomicLong.get() + " atomiclong 耗时 " + (System.currentTimeMillis() - startTime));
        CountDownLatch countDownLatchLongAddr = new CountDownLatch(threadCount);
        startTime = System.currentTimeMillis();
        for (int i = 0; i < threadCount; i++) {
            new Thread(()->{
                for (int i1 = 0; i1 < count; i1++) {
                    longAdder.add(1);
                }
                countDownLatchLongAddr.countDown();
            }).start();
        }
        countDownLatchLongAddr.await();
        System.out.println(longAdder.longValue() + " longaddr 耗时 " + (System.currentTimeMillis() - startTime));
    }
}

100000000 atomiclong 耗时 2292
100000000 longaddr 耗时 421

LongAdder源码分析

上图吧先

在这里插入图片描述
大概就是如果基本技术base cas失败就会开辟数组空间,然后这个线程的id值取模得到cell数组下标去进行累加。

基本数据结构

	static final int NCPU = Runtime.getRuntime().availableProcessors(); //可用的处理器个数

    /**
     * Table of cells. When non-null, size is a power of 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.
     */
    transient volatile long base; //基本计数

    /**
     * Spinlock (locked via CAS) used when resizing and/or creating Cells.
     */
    transient volatile int cellsBusy; //调整大小或者创建cells数组的时候的cas锁变量

这个有个地方值得只注意 cell数组是volatile的, 这样cell数组的扩容赋值之后别的线程就可以立马感知到,也就是可见性, 那问题来了,我对cell数组里面的元素累加的时候呢? 所以依然需要可见性, Cell的类结构如下

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

public void add()

public void add(long x) {
        Cell[] as; long b, v; int m; Cell a;//这里cells是volatile的所以重新赋值保证了一定读到最新的
        if ((as = cells) != null || !casBase(b = base, b + x)) { //如果cell数组已经扩展了 或者 cas成功,说明没有竞争就不会继续往下面走
            boolean uncontended = true; //true表示没有竞争
            if (as == null || (m = as.length - 1) < 0 || //as数组没有初始化, 或者初始化了但是长度为0
                (a = as[getProbe() & m]) == null ||      //进行下标的定位 采用按位于的方式,比%的速度更快, 当前线程对应的槽位cell为null
                !(uncontended = a.cas(v = a.value, v + x)))  //当前下标cell不为null,尝试进行cas操作
                longAccumulate(x, null, uncontended); //uncontended=false 说明当前有竞争,因为cas失败了, 所以总结有几种情况会进入到longAccumulate
        }
    }

预期总结如何才会进入longAccumulate 不如总结如何才不进入。。。
1、base 基本计数cas成功
2、cell槽位cas成功

final void longAccumulate()

final void longAccumulate(long x, LongBinaryOperator fn,
                              boolean wasUncontended) {
        int h;
        if ((h = getProbe()) == 0) { //数组第0个元素, 0和0与才会等于0,所以这里就没有进行与了,直接判断, 不进行初始化默认hash为0
            ThreadLocalRandom.current(); // force initialization 改变线程hash
            h = getProbe();  //这里做了优化,使用ThreadLocalRandom为当前线程重新赋予一个hash值,为啥这样呢。。因为进入这个方法是有可能有竞争的,所以重新hash一下到别的槽位,应该是没有竞争
            wasUncontended = true;   //重新hash到别的cell, 默认是没有竞争的
        }
        boolean collide = false; // True if last slot nonempty 扩容意向。false表示不会扩容,true表示可能会扩容
        for (;;) {
            Cell[] as; Cell a; int n; long v;
            if ((as = cells) != null && (n = as.length) > 0) { //cells数组不为null, 且长度大于0, 说明已经进行了初始化
                if ((a = as[(n - 1) & h]) == null) { //如果计算出来的槽位没有创建
                    if (cellsBusy == 0) {       // Try to attach new Cell 
                        Cell r = new Cell(x);   // Optimistically create 乐观的创建,待会需要进行cas
                        if (cellsBusy == 0 && casCellsBusy()) { //如果现在还是没有别人获取到锁,获取操作cells数组的执行权
                            boolean created = false;
                            try {               // Recheck under lock
                                Cell[] rs; int m, j;
                                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; //这里说明创建的cell本身就已经默认是x值了,所以直接break
                            continue;           // Slot is now non-empty
                        }
                    }
                    collide = false;
                }
                else if (!wasUncontended)       // CAS already known to fail
                    wasUncontended = true;      // Continue after rehash 这个cell位置竞争激烈,需要进行重新hash
                else if (a.cas(v = a.value, ((fn == null) ? v + x :
                                             fn.applyAsLong(v, x)))) //cell位置已经有值了,直接进行cas操作
                    break;
                else if (n >= NCPU || cells != as) //是否已经到达CPU核数上限
                    collide = false;            // At max size or stale
                else if (!collide)
                    collide = true;
                else if (cellsBusy == 0 && casCellsBusy()) {//获取cell的锁权限
                    try {
                        if (cells == as) {      // Expand table unless stale 表的扩展
                            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;                   // Retry with expanded table
                }
                h = advanceProbe(h);// 重新获取hash值
            }
            else if (cellsBusy == 0 && cells == as && casCellsBusy()) { //cells == as 就是代表没有别的线程进行初始化,都是null, casCellsBusy() cas争取获取初始化数组的资格
                boolean init = false;
                try {                           // Initialize table
                    if (cells == as) { //确保没有被初始化?这里这个判断需要好好思考,因为casCellsBusy() 与 cellsBusy = 0 所以这个casCellsBusy()可以多次cas成功,但是我们只需要第一个线程初始化就行
                        Cell[] rs = new Cell[2]; //第一次初始化默认为2个长度数组
                        rs[h & 1] = new Cell(x);//这里的x就是add(x)传入的单位,呃,因为当前线程的就是累加,所以当当前线程拥有了锁去初始化数组权限的时候直接赋值x就行
                        cells = rs; //volatile赋值
                        init = true; //初始化完成
                    }
                } finally {
                    cellsBusy = 0; //初始化已经完成了,把初始化权限释放掉
                }
                if (init)
                    break; //初始化完成跳出循环
            }
            else if (casBase(v = base, ((fn == null) ? v + x : //进入这里的逻辑说明别人已经在对cell进行初始化了,所以当前线程可以进行一下base的cas累加,说不定base已经不激烈了。
                                        fn.applyAsLong(v, x))))
                break; // Fall back on using base  cas 累加到base上成功,直接跳出循环
        }
    }

上图

在这里插入图片描述

总结

分段思想,充分利用cpu。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值