juc-atomic原子类之七:LongAdder

LongAdder基本原理和思想

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

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

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

public void add(long x) {
    // as就类似我们上述说的备用窗口列表
    Cell[] as;
    // 这里的b就是常规窗口的值,v是你被分派到的那个窗口的value值(可能是常规窗口也可能是备用窗口)
    long b, v;
    // m是备用窗口的长度,
    // 上面我们讲过getProbe()方法就是获取用户id的方法
    // getProbe() & m 其实就是 用户id % 窗口总数,窗口分派的算法
    int m;
    // a就是你被派遣到的那个窗口
    Cell a;
    // 1.首先如果cells==null,说明备用窗口没有开放,
    // 全都执行casBase争抢常规窗口,cas成功则争抢成功,然后办完事就退出了
    // 如果争抢失败 casBase == false,则会进入if代码内重试
    // 2. 如果cells != null,说明备用窗口开发了,不用去争抢常规窗口了,
    // 直接就进入争抢备用窗口
    if ((as = cells) != null || !casBase(b = base, b + x)) {
        boolean uncontended = true; //字面意思也可以看出来uncontended意思是没有竞争// 3. as == null || as.length - 1 < 0 说明备用窗口列表尚未开放
        if (as == null || (m = as.length - 1) < 0 ||
            // 4. as[getProbe() & m] 你被派遣到的那个备用窗口
            // (a = as[getProbe() & m]) == null 你被派遣到的那个备用窗口还没人工作
            (a = as[getProbe() & m]) == null ||
            // 5. a.cas() 就是你被分派到窗口a后,去尝试争抢窗口a的权限
            // 如果 uncontented就是你争抢的结果,如果!uncnotented == true,说明你争抢失败了
            !(uncontended = a.cas(v = a.value, v + x)))
            
            // 相当于上面操作都失败之后的一种兜底方案
            longAccumulate(x, null, uncontended);
    }
}

 

流程图:

 

这四部分分别为

  1. as == null:说明备用窗口没有开放
    1. casBase(b = base, b + x):x此时为1,A、casBase是Stiped64的方法
    2.     final boolean casBase(long cmp, long val) {
              return UNSAFE.compareAndSwapLong(this, BASE, cmp, val);
          }

       由于此处的x为1,那么该方法就是对Striped64BASE的值进行累加并返回是否成功,顺带一提这里的BASEbase值所对应的内存偏移量,所以casBase(b = base, b + x)就是对base进行CAS操作,执行成功的话操作就结束了,那么什么时候会不成功呢,当然就是并发量大的时候,结合之前分析的longValue()方法,这里就可以得出一个结论——当并发不大的时候只对base进行更新,获取值得时候当然也只从base获取即可,这个时候其实和AtomicLong的原理几乎一模一样

  2. (m = as.length - 1) < 0
  3. (a = as[getProbe() & m]) == null
  4. !(uncontended = a.cas(v = a.value, v + x))

这里的四个条件其实并不是并列的,而是递进式的,1和2判断cells数组是否为空,3取cells数组中的任意一个元素a判断是否为空,4是对a进行cas操作并将执行结果赋值标志位uncontended。从这里可以给出第二个结论,当竞争激烈到一定程度无法对base进行累加操作时,会对cells数组中某个元素进行更新

 

B、Striped64的cell:我们可以看到一个Cell的类,那这个类是是LongAdder的父类Striped64中的Cell数组类型的成员变量。每个Cell对象中都包含一个volatile的变量的value值,并提供对这个value值的CAS操作,且更改这个变量唯一的方式通过cas。LongAdder的高明之处可能在于将之前单个节点的并发分散到各个节点的(cell数组),这样从而提高在高并发时候的效率。

最后来看一下当上述条件无法全部满足时调用的Striped64.longAccumulate(x, null, uncontended)方法。

 

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

posted on 2014-05-14 21:26  duanxz  阅读(657)  评论(0编辑  收藏  举报