从源码来探讨探讨LongAdder为什么是个高效的加法器

LongAdder是JUC包下的一个加法器,可以用于并发下的计数,但是在多线程的情况下其实获得他的值并不是一定是当前正确的值,毕竟他的sum方法里面对于读取数据没有任何的锁,但是LongAdder是可以保证最终正确性的,也就是每一次的加都是有效的。

可能这边会有人有疑问,既然是加法器,那我直接使用AtomicInteger不是也可以实现吗,为什么还要使用LongAdder呢?
确实,我刚开始也会有疑问,所以我们来做个实验,来测测多线程下这两个的耗时

void testLongAdder() throws InterruptedException {
        LongAdder longAdder = new LongAdder();
        CountDownLatch countDownLatch = new CountDownLatch(5);
        long start = System.currentTimeMillis();
        for (int n = 0; n < 5; n++) {
            new Thread(() -> {
                for (int m = 0; m < 500000; m++) {
                    longAdder.add(1);
                }
                countDownLatch.countDown();
            }).start();
        }
        countDownLatch.await();
        System.out.println(System.currentTimeMillis() - start);
        System.out.println(longAdder.sum());
    }
void testAtomicInteger() throws InterruptedException {
        AtomicInteger atomicInteger = new AtomicInteger();
        CountDownLatch countDownLatch = new CountDownLatch(5);
        long start = System.currentTimeMillis();
        for (int n = 0; n < 5; n++) {
            new Thread(() -> {
                for (int m = 0; m < 500000; m++) {
                    atomicInteger.incrementAndGet();
                }
                countDownLatch.countDown();
            }).start();
        }
        countDownLatch.await();
        System.out.println(System.currentTimeMillis() - start);
        System.out.println(atomicInteger.get());
    }

上面这两段代码差不多,大体的意思就是开5个线程 每个线程 累加500000次,然后看最后的耗时哪个比较长
直接上最后结果了
AtomicInteger每次耗时都是大约100ms多一些
LongAdder每次耗时都在50ms以下
这个差距会随着累加次数的增加而增加,也会随着线程的增加而增加

那么为什么LongAdder会这么高效呢
我们直接看他源码里面的实现
读源码之前,我们先来看一下LongAdder使用到的几个关键的东西
一个是Cell静态内部类,还有一个base变量

@sun.misc.Contended static final class Cell {
		// 当前这个cell里面的值
        volatile long value;
        Cell(long x) { value = x; }
        final boolean cas(long cmp, long val) {
            return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
        }

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

这就是Cell内部类的定义,主要就是一个value的成员变量用来计数用的
还有一个cas的方法就是使用cas来替换value的值
base变量则是当没有竞争的情况下,我们直接加这个变量,没必要使用cell数组分片
结合下面的源码我们来理解

public void add(long x) {
        Cell[] as; long b, v; int m; Cell a;
        // 如果当前cell数组不为空 或者 尝试cas 给base变量替换成加成功之后的值
        if ((as = cells) != null || !casBase(b = base, b + x)) {
            boolean uncontended = true;
            // 这边就判断如果cell数组为null 或者 当前线程操作的cell不为null
            // 那么尝试给cas给这个cell替换值
            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数组为null,那么之前肯定没有发生竞争,那么就尝试直接在base变量上加就可以了。至于他是判断当前这个线程是第几个cell,这边他是通过sun.misc.Unsafe 类然后偏移量获得当前线程里面的threadLocalRandomProbe的值(其实在反射底层也是通过这样子来获得成员变量的值),这个我的理解是这个线程的一个随机值。然后这个值 和 cell数组的长度 取模 运算然后定位到对应的cell,如果对应的cell不为null 并且cas失败 那么就进入到longAccumulate方法,我们进入看看

这个代码就有点长了,慢慢阅读

final void longAccumulate(long x, LongBinaryOperator fn,
                              boolean wasUncontended) {
        int h;
        // 这边也就是获得当前这个线程的threadLocalRandomProbe变量的值
        // 如果为 0 代表还没被初始化
        if ((h = getProbe()) == 0) {
        	// 初始化这个值
            ThreadLocalRandom.current(); 
            h = getProbe();
            wasUncontended = true;
        }
        boolean collide = false;              
        for (;;) {
            Cell[] as; Cell a; int n; long v;
            // 如果cell 数组不为null并且长度大于0
            if ((as = cells) != null && (n = as.length) > 0) {
            	// 如果这个线程对应的cell为null
                if ((a = as[(n - 1) & h]) == null) {
                	// 通过这个变量来判断当前有没有线程在扩容cell 或者也在初始化cell
                    if (cellsBusy == 0) {
                    	// 初始化一个cell变量       
                        Cell r = new Cell(x);
                        // 再次判断 并且使用cas去修改cellsBusy变量 如果修改成功则代表当前线程可以去尝试初始化这个线程对应的cell   
                        if (cellsBusy == 0 && casCellsBusy()) {
                            boolean created = false;
                            try {               
                                Cell[] rs; int m, j;
                                // 再次判断当前这个线程对应的cell有没有被初始化
                                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;
                            // 如果已经有其他线程初始化了 那么继续循环尝试
                            continue;          
                        }
                    }
                    collide = false;
                }
                // 这里我的理解就是在之前cas已经失败了就证明有其他线程也对应这个cell 所以就不再尝试cas了 直接在下面修改当前线程的threadLocalRandomProbe的值也就是尝试一个新的cell对应当前线程
                else if (!wasUncontended)       
                    wasUncontended = true;
                // 如果当前线程的cell 不为null 那么使用cas去尝试加上x
                else if (a.cas(v = a.value, ((fn == null) ? v + x :
                                             fn.applyAsLong(v, x))))
                    // 如果cas成功 就退出
                    break;
                // 如果上一步cas失败 并且当前cell的长度已经大于等于cpu的核心数(NCPU这个变量就是核心数的大小)
                // 或者 cell 已经被改变过 其实也就是扩容过了(其实这里满足了也就是不用再往下判断了)
                else if (n >= NCPU || cells != as)
                    collide = false;
                // 这里我觉得是给一次机会 不扩容cell数组 默认collide就为false        
                else if (!collide)
                    collide = true;
                // 这步就是 判断当前状态是否没有线程在扩容或者初始化cell
                // 如果没有的话使用cas去修改状态值
                else if (cellsBusy == 0 && casCellsBusy()) {
                    try {
                    	// 判断之前没有线程修改cell数组的引用 也就是之前没有线程已经扩容了
                        if (cells == as) {
                        	// 两倍扩容
                            Cell[] rs = new Cell[n << 1];
                            // 将之前的迁移到新的cell数组里面
                            for (int i = 0; i < n; ++i)
                                rs[i] = as[i];
                            cells = rs;
                        }
                    } finally {
                        cellsBusy = 0;
                    }
                    collide = false;
                    continue;                   
                }
                // 修改当前线程的threadLocalRandomProbe的值也就是尝试一个新的cell对应当前线程
                h = advanceProbe(h);
            }
            // 如果当前cell数组为null 并且状态为0 使用cas尝试修改状态
            else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
                boolean init = false;
                try {
                	// 再次判断cell数组没有被修改过                       
                    if (cells == as) {
                    	// 初始化长度为2的数组
                        Cell[] rs = new Cell[2];
                        // 根据奇偶初始化一个新的cell
                        rs[h & 1] = new Cell(x);
                        cells = rs;
                        init = true;
                    }
                } finally {
                	// 把状态修改回来
                    cellsBusy = 0;
                }
                // 如果初始化成功 就退出循环
                if (init)
                    break;
            }
            // 如果当前cell数组为null 并且初始化也没有轮到当前线程那就暂时 加到base变量
            else if (casBase(v = base, ((fn == null) ? v + x :
                                        fn.applyAsLong(v, x))))
                break;                          // Fall back on using base
        }
    }

经过上面的源码分析,我们可以总结一下 LongAdder高效的原因

如果没有竞争的情况下 直接使用cas操作base变量
如果在有竞争的情况下,尽量把每个线程的操作分散到不同的cell,这样子就会避免竞争,最优的情况下,如果cpu的核心八个核心,当同时来调用add方法,如果每个核心的线程都是不同的cell那效率就非常高了。如果线程确实多
然后各种冲突他也会进行扩容,但是最多也只是扩到cpu的核心数

其实还有一个细节的点,就是cell类上面还有一个@sun.misc.Contended
这个注解,这个注解是为了解决一种伪共享的情况,这边解释一下这个现象,就是cpu底层有缓存,缓存是以缓存行为单位的(常见的缓存行一行为64个字节),如果不加这个注解,那么这个cell数组可能会一部分在同一个缓存行里面,那么基于MESI协议那么如果cpu更新了这个缓存里的数据就会造成相应的这行的缓存行失效,所以即使 cpu的不同核心更新的是不同cell,那么也会造成缓存行的失效,那么其他核心发现缓存行的失效,就得去主存同步新的数据回来。这样子就会损失效率,@sun.misc.Contended这个注解就是为了解决这个问题,也就是去填充缓存行,使得一行只可以放得下一个cell,也就解决了伪共享的问题

以上就是我本篇文章的内容,如果有不对的地方,欢迎指正

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值