LongAdder原理

前面介绍了Java中的一些原子类,但是基本都是通过CAS来实现原子性操作,白白浪费CPU资源。JDK8中新增了一个原子性递增或者递减类LongAdder用来克服高并发下使用AtomicLong的缺点。LongAdder的思路是把一个变量分解为多个变量,让同样多的线程去竞争多个资源。如图所示:
LongAdder原理
使用LongAdder时,内部维护了多个Cell变量,每个Cell里面有一个初始值为0的long型变量,这样同时争取一个变量的线程就变少了,而是分散成对多个变量的竞争,减少了失败次数。如果竞争某个Cell变量失败,它不会一直在这个Cell变量上自旋CAS重试,而是尝试在其他的Cell变量上进行CAS尝试,这个改变增加了当前线程重试CAS成功的可能性。最后,在获取LongAdder当前值时,是把所有Cell变量的value值累加后再加上base返回的

LongAdder里面有一个Cell数组,是惰性加载的,即需要时创建。当并发线程较少时,所有累加操作都是针对base变量进行。Cell类型是AtomicLong的一个改进,用来减少缓存的争用,也就是解决伪共享问题。因为Cell数组元素的内存地址是连续的,所以数组内的多个元素能经常共享缓存行,因此这里使用@sun.misc.Contended注解对Cell类进行字节填充,防止数组中多个元素共享一个缓存行,提升性能。

LongAdder代码分析

接下来的的解析都围绕着六个问题来进行:

  1. LongAdder的结构是怎样的?
  2. 线程访问Cell数组的哪一个Cell元素?
  3. 如何初始化Cell数组?
  4. Cell数组如何扩容?
  5. 线程访问分配的Cell元素有冲突后如何处理?
  6. 如何保证线程操作被分配的Cell元素的原子性?

在这里插入图片描述
LongAdder继承自Striped64,在Striped64中维护者三个变量:basecellsBusyCell数组。base是个基础知识,默认为0.cellsBusy用来实现自旋锁,状态值只有0和1,当创建Cell元素,扩容Cell数组或者初始化Cell数组时,使用CAS操作该变量来保证同时只有一个线程可以进行其中之一的操作。所以cells是volatile的,但没有加锁,而是用的自旋锁。

看一下Cell的构造

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

很简单,一个Cell维护一个volatile的变量,和Cell数组类似,这里设置成volatile但并没有加锁主要是利用了cas来保证内部value的原子性更改,这也回答了问题6。类Cell使用@sun.misc.Contended修饰是为了避免伪共享,因为Cell数组是连续的,很容易出现这种问题。我们也弄懂了问题1。接下来看一下LongAdder里面的几个方法:

  • long sum():这是计算总和的,即将所有Cell里面的元素和base一起加起来。由于计算时没有对Cell数组加锁,所以累加过程中其他线程可能修改Cell的值或对数组扩容,所以sum返回的值不精确,并不是一个原子快照。
  • void reset():把base置0,并把Cell数组内有元素的都重置为0。
  • long sumThenReset():就是sum()和reset()合起来,求和的过程中重置。由于也没有加锁,该方法在多线程环境下会有问题,比如一个线程执行了这个方法,而另一个线程紧随着也执行这个方法则总会返回0.
  • longValue():等价于sum()
  • add():这个方法是加一个数,看一下它是如何执行的。
    public void add(long x) {
        Cell[] as; long b, v; int m; Cell a;
        if ((as = cells) != null || !casBase(b = base, b + x)) {	//(1)
            boolean uncontended = true;
            if (as == null || (m = as.length - 1) < 0 ||	//(2)
                (a = as[getProbe() & m]) == null ||			//(3)
                !(uncontended = a.cas(v = a.value, v + x)))	//(4)
                longAccumulate(x, null, uncontended);		//(5)
        }
    }

要注意这个函数里面的判断条件虽然是或的关系,其实是层层递进的。

  1. 代码(1)先看cells是否为null,如果不为空则直接执内部代码块,为空则先尝试在base变量上进行累加。
  2. 如果累加base变量成功了,则直接返回,失败了则执行内部代码块。
  3. 代码(2)如果cells是空的(as==null或者as的长度为0),则执行代码(5)进行累加;不为空则得到要当前线程要访问的cell(即变量a,下标是通过getProb() & m获取的)。
  4. 如果要访问的cell为空,则执行代码(5),否则就对访问的cas进行原子改变操作,并返回uncontended变量查看是否执行成功,成功了直接返回,失败了还是要执行代码(5)。

这个地方我们就回答了问题2,即如何知道当前线程是访问哪一个cell呢,通过getProbe() & m获取下标,进而从cells数组中取得。m代表当前cells数组元素个数-1,getProbe()用于获取当前线程中变量threadLocalRandomProbe的值,它一开始为0,在代码5中会对其进行初始化。


接下来就是代码5的部分了,也是很复杂的,它主要用于初始化cells数组和扩容

final void longAccumulate(long x, LongBinaryOperator fn,
                              boolean wasUncontended) {
        //(6) 初始化当前线程的变量threadLocalRandomProbe的值
        int h;
        if ((h = getProbe()) == 0) {
            ThreadLocalRandom.current(); // force initialization
            h = getProbe();
            wasUncontended = true;
        }
        boolean collide = false;                // True if last slot nonempty
        for (;;) {
            Cell[] as; Cell a; int n; long v;
            if ((as = cells) != null && (n = as.length) > 0) {	//(7)
                if ((a = as[(n - 1) & h]) == null) {	//(8)
                    if (cellsBusy == 0) {       // Try to attach new Cell
                        Cell r = new Cell(x);   // Optimistically create
                        if (cellsBusy == 0 && casCellsBusy()) {
                            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;
                            continue;           // Slot is now non-empty
                        }
                    }
                    collide = false;
                }
                else if (!wasUncontended)       // CAS already known to fail
                    wasUncontended = true;      // Continue after rehash
                //当前Cell存在,则执行CAS设置(9)
                else if (a.cas(v = a.value, ((fn == null) ? v + x :
                                             fn.applyAsLong(v, x))))
                    break;
                //当前Cells数组元素个数大于CPU个数(10)
                else if (n >= NCPU || cells != as)
                    collide = false;            // At max size or stale
                else if (!collide)
                //是否有冲突(11)
                    collide = true;
                //如果当前元素个数没有达到CPU个数并且有冲突则扩容(12)
                else if (cellsBusy == 0 && casCellsBusy()) {
                    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
                }
                //(13)为了能够找到一个空闲的Cell,重新计算hash值,xorshift算法生成随机数
                h = advanceProbe(h);
            }
            //初始化Cell数组(14)
            else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
                boolean init = false;
                try {                           // Initialize table
                    if (cells == as) {
                    	//14.1
                        Cell[] rs = new Cell[2];
                        //14.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))))
                break;                          // Fall back on using base
        }
    }

代码很长很复杂,这里我们还是针对开头提出的问题3、问题4、问题5。

  • 问题3:cells数组初始化是在代码(14)中进行的,cellsBusy是一个标示,为0说明当前cells数组没有在被初始化或者扩容,也没有在新建Cell元素(没有发生结构化改变),为1说明cells数组再被扩容、初始化或创建新的Cell元素,只有通过casCellsBusy()函数将cellsBusy变量成功由0设置为1才可继续初始化。然后就是申请数组空间,然后计算本线程要访问的cell下标,设置初始化标志,最后要重置cellsBusy标记
  • 问题4:cells数组扩容是在代码(12)中进行的,对cells扩容是有条件的,也就是代码(10)(11)的条件都不满足,即当前cells的元素个数小于当前机器CPU个数并且当前多个线程访问了cells中同一个元素,从而导致冲突使其中一个线程CAS失败时才会进行扩容操作。因为最好让每个CPU只运行一个线程时效果最好,扩容也会进行casCellsBusy()操作。扩容后将原来的复制到新的数组中,剩余的还未初始化,为Null。
  • 问题5:代码(7)(8)中,当前线程调用add方法并根据当前线程的随机数threadLocakRandomProbe和cells元素个数计算要访问的cell元素下标,然后如果发现对应下标元素的值为null,则新增一个Cell元素到cells数组,并且在将其添加到cells数组之前要竞争设置cellsBusy为1。代码(13)对CAS失败的线程重新计算当前线程的随机值threadLocalRandomProbe,以减少下次访问cells元素时的冲突机会
  • 13
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值