• AtomicLong和LongAdder的区别


    前言

      最近在看到不少框架里面使用到了LongAdder这个类,而并非AtomicLong,很是困惑,于是专门看了LongAdder的源码,总结一下这两个的区别。

    AtomicLong原理

      就像我们所知道的那样,AtomicLong的原理是依靠底层的cas来保障原子性的更新数据,在要添加或者减少的时候,会使用死循环不断地cas到特定的值,从而达到更新数据的目的。那么LongAdder又是使用到了什么原理?难道有比cas更加快速的方式?

    LongAdder基本原理和思想

    我们都知道AtomicLong是通过无限循环不停的采取CAS的方法去设置value,直到成功为止。那么当并发数比较多或出现更新热点时,就会导致CAS的失败机率变高,重试次数更多,越多的线程重试,CAS失败的机率越高,形成恶性循环,从而降低了效率。而LongAdder的原理就是降低对value更新的并发数,也就是将对单一value的变更压力分散到多个value值上,降低单个value的“热度”
    我们知道LongAdder的大致原理之后,再来详细的了解一下它的具体实现,其中也有很多值得借鉴的并发编程的技巧。

    首先我们来看一下LongAdder有哪些方法?

      可以看到和AtomicLong基本类似,同样有增加、减少等操作,那么如何实现原子的增加呢?

    public void add(long x) {
        Cell[] as; long b, v; int m; Cell a;
        if ((as = cells) != null || !casBase(b = base, b + x)) { //step1
            boolean uncontended = true;
            if (as == null || (m = as.length - 1) < 0 || //step2
                (a = as[getProbe() & m]) == null ||  //step3
                !(uncontended = a.cas(v = a.value, v + x))) //step4
                longAccumulate(x, null, uncontended); // step5
        }
    }

    我们可以看到一个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);
                }
            }
        }

    cellsLongAdder的父类Striped64中的Cell数组类型的成员变量。每个Cell对象中都包含一个volatile的变量的value值,并提供对这个value值的CAS操作,且更改这个变量唯一的方式通过cas。

    回到LongAdder,我们可以猜测到LongAdder的高明之处可能在于将之前单个节点的并发分散到各个节点的(cell数组),这样从而提高在高并发时候的效率。

    下面我们来验证我们的观点,我们接着看上图的add方法,

    if ((as = cells) != null || !casBase(b = base, b + x)) {

    如果cell数组不为空,那么就尝试更新base元素,如果更新成功,那么就直接返回。base元素在这里起到了一个什么作用呢?可以保障的是在低并发的时候和AtomicLong一样的直接对基础元素进行更新。 

    可以认为变量base就是第一个value值,也是基础value变量。先调用casBase函数来cas一下base变量,如果成功了,就不需要在进行后面比较复杂的算法。

    casBase()方法:

        /**
         * CASes the base field.
         */
        final boolean casBase(long cmp, long val) {
            return UNSAFE.compareAndSwapLong(this, BASE, cmp, val);
        }

    step2if (as == null || (m = as.length - 1) < 0 || //step2

    然后我们继续看step2第二层条件语句中执行的逻辑。如果cells数组为null或为空,就直接调用longAccumulate方法。因为cells为null或在为空,说明cells未完全初始化,所以调用longAccumulate进行初始化。否则继续判断。

    如果cells中已经有对象了,那么执行step3。我们先来理解一下getProbe() & m的这个操作吧。我们可以首先将这个操作当作一次计算"hash"值,然后将cells中这个位置的Cell对象赋值给变量a。然后判断a是否为null,如果不为null,那么就调用Cell对象自己的cas方法去设置value值。如果a为null,或在cas赋值发生冲突,那么也是开始调用longAccumulate方法。否则就会进入Striped64.longAccumulate()方法。 
    longAccumulate比较复杂,
    首先,我们都知道只有当对base的cas操作失败之后,LongAdder才引入Cell数组.所以在longAccumulate中就是对Cell数组进行操作.分别涉及了数组的初始化,扩容和设置某个位置的Cell对象等操作.
    在这段代码中,关于cellBusy的cas操作构成了一个SpinLock,这就是经典的SpinLock的编程技巧,大家可以学习一下.
    final void longAccumulate(long x, LongBinaryOperator fn,
                                 boolean wasUncontended) {
           int h;
           if ((h = getProbe()) == 0) { //获取PROBE变量,探针变量,与当前运行的线程相关,不同线程不同
               ThreadLocalRandom.current(); //初始化PROBE变量,和getProbe都使用Unsafe类提供的原子性操作。
               h = getProbe();
               wasUncontended = true;
           }
           boolean collide = false;
           for (;;) { //cas经典无限循环,不断尝试
               Cell[] as; Cell a; int n; long v;
               if ((as = cells) != null && (n = as.length) > 0) { //分支1:cells数组不为null,并且数组size大于0
               //表示cells已经初始化了
                   if ((a = as[(n - 1) & h]) == null) { //通过与操作计算出来需要操作的Cell对象的坐标
                       if (cellsBusy == 0) { //volatile 变量,用来实现spinLock,来在初始化和resize cells数组时使用。
                       //当cellsBusy为0时,表示当前可以对cells数组进行操作。 
                           Cell r = new Cell(x);//将x值直接赋值给Cell对象
                           if (cellsBusy == 0 && casCellsBusy()) {//如果这个时候cellsBusy还是0
                           //就cas将其设置为非0,如果成功了就是获得了spinLock的锁.可以对cells数组进行操作.
                           //如果失败了,就会再次执行一次循环
                               boolean created = false;
                               try {
                                   Cell[] rs; int m, j;
                                   //判断cells是否已经初始化,并且要操作的位置上没有cell对象.
                                   if ((rs = cells) != null &&
                                       (m = rs.length) > 0 &&
                                       rs[j = (m - 1) & h] == null) {
                                       rs[j] = r; //将之前创建的值为x的cell对象赋值到cells数组的响应位置.
                                       created = true;
                                   }
                               } finally {
                                   //经典的spinLock编程技巧,先获得锁,然后try finally将锁释放掉
                                   //将cellBusy设置为0就是释放锁.
                                   cellsBusy = 0;
                               }
                               if (created)
                                   break; //如果创建成功了,就是使用x创建了新的cell对象,也就是新创建了一个分担热点的value
                               continue; 
                           }
                       }
                       collide = false; //未发生碰撞
                   }
                   else if (!wasUncontended)//是否已经发生过一次cas操作失败
                       wasUncontended = true; //设置成true,以便第二次进入下一个else if 判断
                   else if (a.cas(v = a.value, ((fn == null) ? v + x :
                                                fn.applyAsLong(v, x))))
                       //fn是操作类型,如果是空,就是相加,所以让a这个cell对象中的value值和x相加,然后在cas设置,如果成果
                      //就直接返回
                       break;
                   else if (n >= NCPU || cells != as)
                     //如果cells数组的大小大于系统的可获得处理器数量或在as不再和cells相等.
                       collide = false;            // At max size or stale
                   else if (!collide)
                       collide = true;
                   else if (cellsBusy == 0 && casCellsBusy()) {
                     //再次获得cellsBusy这个spinLock,对数组进行resize
                       try {
                           if (cells == as) {//要再次检测as是否等于cells以免其他线程已经对cells进行了操作.
                               Cell[] rs = new Cell[n << 1]; //扩容一倍
                               for (int i = 0; i < n; ++i)
                                   rs[i] = as[i];
                               cells = rs;//赋予cells一个新的数组对象
                           }
                       } finally {
                           cellsBusy = 0;
                       }
                       collide = false;
                       continue;
                   }
                   h = advanceProbe(h);//由于使用当前探针变量无法操作成功,所以重新设置一个,再次尝试
               }
               else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
               //cells数组未初始化,获得cellsBusy lock,来初始化
                   boolean init = false;
                   try {                           // Initialize table
                       if (cells == as) {
                           Cell[] rs = new Cell[2];
                           rs[h & 1] = new Cell(x); //设置x的值为cell对象的value值
                           cells = rs;
                           init = true;
                       }
                   } finally {
                       cellsBusy = 0;
                   }
                   if (init)
                       break;
               }//如果初始化数组失败了,那就再次尝试一下直接cas base变量,如果成功了就直接返回
               else if (casBase(v = base, ((fn == null) ? v + x :
                                           fn.applyAsLong(v, x))))
                   break;                          // Fall back on using base
           }
       }

    上面的代码主要有三个分支: 
    分支一、 如果数组不为空 
    分支二、 数据为空,则初始化 
    分支三、 前面都更新失败了,尝试更新base数据 
    4.cellBusy是一个标示元素,只有当修改cell数组大小或者插入元素的时候才会修改。

        /**
         * CASes the cellsBusy field from 0 to 1 to acquire lock.
         */
        final boolean casCellsBusy() {
            return UNSAFE.compareAndSwapInt(this, CELLSBUSY, 0, 1);
        }

    分支二、分支三都很简单,我们来重点分析一下分支一。 
    当要更新的位置没有元素的时候,首先cas标志位,防止扩容以及插入元素,然后插入数据。如果成功直接返回,否则标示发生了冲突,然后重试。如果对应的位置有元素则更新,如果更新失败,进行判断是否数组的大小已经超过了cpu的核数,如果大于的话,则意味着扩容没有意义。直接重试。否则进行扩容,扩容完成后,重新设置要更新的位置,尽可能保证下一次更新成功。 

    回到LongAdder类,我们来看一下如何统计计数的sum方法:

        /**
         * Returns the current sum.  The returned value is <em>NOT</em> an
         * atomic snapshot; invocation in the absence of concurrent
         * updates returns an accurate result, but concurrent updates that
         * occur while the sum is being calculated might not be
         * incorporated.
         *
         * @return the sum
         */
        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;
        }

    当计数的时候,将base和各个cell元素里面的值进行叠加,从而得到计算总数的目的。这里的问题是在计数的同时如果修改cell元素,有可能导致计数的结果不准确。

    总结:

    LongAdder在AtomicLong的基础上将单点的更新压力分散到各个节点,在低并发的时候通过对base的直接更新可以很好的保障和AtomicLong的性能基本保持一致,而在高并发的时候通过分散提高了性能。 
    缺点是LongAdder在统计的时候如果有并发更新,可能导致统计的数据有误差。

  • 相关阅读:
    [置顶] windows player,wzplayerV2 for windows
    wzplayer 近期将会支持BlackBerry和WinPhone8
    wzplayerEx for android(真正硬解接口,支持加密的 player)
    ffmpeg for ios 交叉编译 (支持i686 armv7 armv7s) 包含lame支持
    ffmpeg for ios 交叉编译 (支持i686 armv7 armv7s) 包含lame支持
    编译cegcc 0.59.1
    wzplayer 近期将会支持BlackBerry和WinPhone8
    wzplayerEx for android(真正硬解接口,支持加密的 player)
    windows player,wzplayerV2 for windows(20140416)更新
    编译cegcc 0.59.1
  • 原文地址:https://www.cnblogs.com/duanxz/p/3724446.html
Copyright © 2020-2023  润新知