• 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在统计的时候如果有并发更新,可能导致统计的数据有误差。

  • 相关阅读:
    HDFS上传文件
    SparkStreaming和Kafka的整合
    hadoop ha集群搭建
    Zookeeper安装
    hadoop完全分布式集群的搭建
    hadoop伪分布式集群的搭建
    java笔记之字符串,gc
    java笔记之字符串,gc
    java基础之short案例分析以及final关键字
    java基础之short案例分析以及final关键字
  • 原文地址:https://www.cnblogs.com/duanxz/p/3724446.html
Copyright © 2020-2023  润新知