高并发计数器之LongAdder源码解析

1.AtomicLong简单分析

AtomicLong是Java1.5时的一个基于CAS的原子类,通过CAS算法提供了非阻塞的原子性操作,但是在超高并发下AtomicLong的性能就会非常低下。

我们来看一下AtmoicLong的 incrementAndGet() 方法的底层实现。(原子类的核心变量就是一个volatile修饰的变量,这里就不在看源码了)

public final long incrementAndGet() {
        return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
}

底层调用的是UnSafe类的getAndAddLong()方法, (UnSafe底层核心都是native方法,即调用了C++的方法)可以看到,底层就是通过不断的自旋(循环),通过CAS一直尝试更新。

//UnSafe类中的方法 
public final long getAndAddLong(Object var1, long var2, long var4) {
        long var6;
        do {
            var6 = this.getLongVolatile(var1, var2); //获取变量最新的值
          //while循环 CAS失败一直不断尝试更新变量
        } while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4)); 
        return var6;
    }

compareAndSwapLong() 就是Unsafe类中的一个native方法,是一个CAS操作。

//UnSafe类中的方法
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

通过简单的源码分析,我们应该可以分析出来,在超高并发下为什么AtomicLong的性能并不高的原因,因为N多线程去操作一个变量会造成大量线程CAS失败,然后一直处于自旋状态(一直占用CPU),导致严重浪费CPU资源,降低了并发性

2.LongAdder与AtmoicLong介绍

先看一下两者的对比

在这里插入图片描述

我们知道,volatile是轻量级锁,可以解决多线程内存不可见的问题,对于一写多读,可以解决变量的同步问题,但是如果是多写,volatile无法解决线程安全问题。

例如,在多线程下的cnt++操作,就应该使用原子类或者加锁处理

// 基于CAS
AtmoicInteger cnt = new AtmoicInteger(); 
cnt.addAndAdd(n);

//加锁
synchronized(){
    cnt ++;
}

而如果是JDK8,推荐使用LongAdder对象代替,因为他的性能比AtomicLong更好(减少乐观锁(CAS)重试次数)

LongAdder其他应用场景

对于Java项目中计数统计的一些需求,如果是JDK8,推荐使用LongAdder对象,比AtomicLong性能更好。在大多数项目及开源组件中,计数统计最多的还是AtomicLong,虽然是阿里这样说,但是我们仍然要根据使用场景来决定是否使用LongAdder。

LongAdder与AtmoicLong性能测试

路飞大佬笔记测试结果

我们可以看到,随着并发数量的增加,AtomicLong的性能是急剧下降的,LongAdder性能是AtomicLong的数倍。

3.LongAdder原理分析

先看一下LongAdder的原理图

在这里插入图片描述

设计思想

LongAdder使用分段的方式降低CAS失败的频次.

我们知道,AtomicLong中有个内部变量value保存着实际的值,所有的操作都是针对该变量进行的。也就是说说,在高并发环境下,value变量其实是一个热点数据,也就是多个线程竞争一个热点

LongAdder的基本思路就是分散热点,将value的值的新增操作分散到一个数组中,不同的线程会命中到数组的不同槽中,各个线程只会对自己槽中的那个value进行CAS操作,这样热点就被分散了,冲突的概率就小很多。

LongAdder中有一个全局变量,volatile long base值,在并发不高的情况下都是通过CAS来直接操作base值,如果CAS失败,则针对LongAdder中的Cell[]数组中的Cell进行CAS操作,减少失败的概率。

例如当前类中base=10,有三个线程进行CAS原子性的 +1操作,线程一执行成功,线程二、三执行失败后针对Cell[]数组中的Cell对象的value值进行CAS操作,此时两个线程对应的Cell对象的value都被设置为1,执行完毕后,统计累加数据: sum = 11 + 1 + 1 = 13

在这里插入图片描述

使用Contended注解消除伪共享

在Cell类上,标注了一个@Contended注解,此注解可以进行缓存行填充,从而解决伪共享问题,伪共享会导致缓存行失效,缓存一致性开销大。
什么是缓存伪共享?

@sun.misc.Contended static final class Cell{}

伪共享是指多个线程同时读一个缓存行的不同变量时导致的CPU缓存失效,尽管这些变量之间没有关系,但由于在主存中临近,存在于同一个缓存行之中,它们的相互导致频繁的缓存未命中,引发性能下降。

解决伪共享的方法一般都是直接填充,我们只需要保证不同线程的变量存在不同的缓存行即可使用多余的字节来填充可以做到这一点,这样就不会出现伪共享的问题。例如在Disruptor队列的设计中就有类似设计。

在这里插入图片描述

在这里插入图片描述

在Cell类上加的注释也有说明这一点
在这里插入图片描述

框中的翻译如下:

Cell类是AtomicLong添加了padded(via@sun.misc.compended)来消除伪共享的变种版本。缓存行填充对于大多数原子来说是繁琐的,因为它们通常不规则地分散在内存中,因此彼此之间不会有太大的干扰。但是,驻留在数组中的原子对象往往彼此相邻,因此在没有这种预防措施的情况下,通常会共享缓存行数据(对性能有巨大的负面影响)。

3、惰性求值

LongAdder只有在使用longValue()获取当前累加值时才会真正的去结算计数的数据,longValue()方法底层就是调用sum()方法,对baseCell数组的数据累加然后返回,做到数据写入和读取分离。

AtomicLong使用incrementAndGet()每次都会返回long类型的计数值,每次递增后还会伴随着数据返回,增加了额外的开销。

LongAdder求和原理

之前说了,AtomicLong是多个线程针对单个热点值value进行原子操作。而LongAdder是每个线程拥有自己的槽,各个线程一般只对自己槽中的那个值进行CAS操作

比如有三个线程同时对value增加1,那么value = 1 + 1 + 1 = 3

但是对于LongAdder来说,内部有一个base变量,一个Cell[]数组。
base变量:非竞争条件下,直接累加到该变量上
Cell[]数组:竞争条件下,累加个各个线程自己的槽Cell[i]
最终结果的计算是下面这个形式:

在这里插入图片描述

4.LongAdder源码解析

4.1Striped64的内部结构

LongAdder继承了Striped64,一些核心的属性都在Striped64中,我们先来看一下Striped64类中的核心结构。

    //内部类 cell内部类
    @sun.misc.Contended static final class Cell {
        //真正的value值
        volatile long value;
        Cell(long x) { value = x; }
        //CAS操作对cell中的value进行赋值
        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);
            }
        }
    }

    //当前计算机的CPU数量,用于控制cells数组长度的一个关键条件。
    static final int NCPU = Runtime.getRuntime().availableProcessors();

    //cells数组
    transient volatile Cell[] cells;

    //当没有发生过竞争时,数据会累加到base中 或者当cells扩容时,需要将数据写到base中
    transient volatile long base;

    /* 
     * 初始化cells数组或者对cells数组扩容时需要和获取一把锁(同步状态) CAS操作
     * 0表示无锁状态,1表示其他线程已经持有锁了。
     */
    transient volatile int cellsBusy;

    /**
     * Package-private default constructor
     */
    Striped64() {
    }
    
    //通过CAS的方式更新cell中的数据
    final boolean casBase(long cmp, long val) {
        return UNSAFE.compareAndSwapLong(this, BASE, cmp, val);
    }

    //通过CAS的方式获取锁(同步状态)
    final boolean casCellsBusy() {
        return UNSAFE.compareAndSwapInt(this, CELLSBUSY, 0, 1);
    }

4.2add()方法

然后我们分析一下LongAdder类中的核心方法之一 add()方法的逻辑

add方法逻辑简单,主要是判断Cell数组是否被创建或者尝试写base,如果写Cell数组不为null或者CAS方式向base中写数据失败,那么接下来要执行核心的longAccumulate方法,否则就是Cell数组没有被初始化并且CAS方式写base成功,代表这次写数据成功。

      // x表示要增加的值
      public void add(long x) {
        /* 
		 *  as 表示cells数组的引用	        
         *  b 表示获取的base值
         *  v 表示期望值
         *  m 表示cells数组的长度
         *  a 表示当前线程命中的cell
         */
        Cell[] as; long b, v; int m; Cell a;
        
        /*   
         *  进入if语句中的条件
         *  条件1:
         *     > true当前cells数组已经创建 当前线程应该将数据写入到对应的cell中,进入if语句操作
         *     > false: 表示cells未初始化,当前线程应该将数据写到base中,执行条件二,写数据到base中
         *
         *  条件2:(取反)
         *       true 表示当前线程CAS写数据到base失败,需要进入if中操作
         *       false 表示当前线程CAS写数据成功,无需进入if中	
         */  
        if ((as = cells) != null || !casBase(b = base, b + x)) {
            /*
             * 什么时候会进入if语句 
             *  1. 当前cells数组已经初始化,当前线程需要将数据写到对应的ecll中
             *  2. CAS写base失败,可能需要重试或者初始化cells数组
             *  
             *  即当前线程写数据失败时(向base中或者cell中写)会进入if语句中
             */
            
            //true表示未竞争  false发生竞争
            boolean uncontended = true;
            
            /*
             *  进入longAccumulate()中的条件
             *
             *  条件一:cells数组还没有初始化
             *  (as == null || (m = as.length - 1) < 0) 为true 
             * 
             *  
             *  条件二:当前线程对应的cells数组的位置为null
             *  getProobe()方法,底层调用的是UnSafe的本地方法getInt(),获取一个整型值,
             *  和m(cells数组的长度-1)的结果就是[0, length - 1]刚好就是下标,注意cells的长度一定是2的次方数 
             *  (跟HashMap的寻址方式类似)
             *     (a == as[getProobe() & m] == null)
             *  
             *  条件三:当前线程对应的cells数组中的位置不为null,但是在对当前cell进行CAS设置值的时候失败,
             *  表示对这一个cell写数据出现了竞争。
             *  !(uncontended = a.cas(v = a.value, v + x))
             */
            if (as == null || (m = as.length - 1) < 0 ||
                (a = as[getProbe() & m]) == null ||
                !(uncontended = a.cas(v = a.value, v + x)))
                
                /*
                 *  那些情况会调用longAccumulate()
                 *  
                 *  1、cells数组未初始化,也就是多线程写base发生了竞争。
                 *  2、cells数组已经初始化,但是当前线程对应的cell为空
                 *  3、cells数组已经初始化,并且当前线程对应的cell不为空,但是对这个cell进行CAS写时失败了
                 */  
                
                longAccumulate(x, null, uncontended);
        }
    }

4.3longAccumulate()方法

通过上面的add()方法可以知道,最核心的方法就是longAccumulate()方法,它在LongAdder的父类Striped64中

总结

1.哪些情况会调用longAccumulate方法(前提条件都是当前线程写值(写到base或者cell中)失败)

  • ①Cell数组未初始化。
  • ②Cell数组初始化了,但是当前线程对应的Cell为NULL
  • ③Cell数组初始化了,且当前线程对应的Cell不为NULL,但是发现有多个线程去写Cell,即发生了竞争。

进入longAccumulate方法后最终会执行怎样的逻辑?

  • 如果Cell数组初始化并且当前线程获取了锁那么当前线程会初始化Cell数组,默认长度是2,并且为当前线程对应的Cell对象进行赋值 对应下面的CASE2
  • 当前Cell数组正在初始化(即锁被其他线程获取),所以当前线程就去执行caseBase()方法通过CAS去更新base. 对应CASE3
  • 对于CASE1来说,又分为几种情况,由于整个过程比较复杂,这里找了一张流程图。

在这里插入图片描述

源码解析

      /*   都有哪些情况会调用?
       *   1、cells未初始化
       *   2、cells初始化了,但是当前线程对应的cell为空,
	   *   3、cells初始化了,并且当前线程对应的cell不为空,但是当前的这个cell发生了写竞争
	   */	
		
   		//wasUncontended: 只有第三种情况即发生了竞争wasUncontended才会变为false。
		final void longAccumulate(long x, LongBinaryOperator fn,
                              boolean wasUncontended) {
        // 表示当前线程的hash值    
        int h;
        
        //为0,说明还没有分配哈希值,下面进行分配哈希值
        if ((h = getProbe()) == 0) {
            ThreadLocalRandom.current(); // force initialization
            h = getProbe(); //赋值给h
            
   /*
    *    这里为当前线程分配完哈希值后,为什么要修改 wasUncontended = true(表示未发生竞争)?
    *    因为走到这里说明当前线程还没有分配哈希值,即默认是0,(0 & 任何数都是0),即当前线程
    *    会被分配到索引为0的cell中,说明可能在索引为0的位置发生了写cell冲突,但是一旦分配
    *    了线程哈希值后,可能下面线程就不会分配到索引为0的位置了,就将标志位修改为true了,表示没有发生竞争。   
    */
            wasUncontended = true; //标志位置为true
        }
            
        /*  
         *  collide表示扩容意向,false一定不会扩容,true可能会扩容。
         */
        boolean collide = false;    
         
            
        //死循环(自旋)    
        for (;;) {
            
            /* 
             * as表示 cells数组的引用
             * a 表示当前线程命中的cell
             * n 表示cells数组长度
             * v 表示期望值
             */
            Cell[] as; Cell a; int n; long v;
            
            
 //---------------------------CASE1 Start-------------------------------------
          //进入条件: cells已经初始化了,当前线程应该将数据写入到对应的cell中
            if ((as = cells) != null && (n = as.length) > 0) {
                
                //CASE1.1 cells已经初始化,但是线程对应索引的cell未初始化
                if ((a = as[(n - 1) & h]) == null) {
                    
                    //true(cellsBusy = 0)表示当前锁未被占用, false表示被占用
                    if (cellsBusy == 0) { 
                        
                        //创建一个cell,初始值就是x。
                        Cell r = new Cell(x);  
                        
                        //只有锁标志位为0并且CAS获取锁成功才能进入下面的逻辑
                        if (cellsBusy == 0 && casCellsBusy()) {
                            
                            boolean created = false;
                                                         
                            try {    
                               /*
                                * rs cells数组的引用
                                * m  cells数组的长度
                                * j  当前线程命中cell的下标
                                */  
                                Cell[] rs; int m, j;
                                
                                /*
                                 * 前两个条件恒成立
                                 * 第三个条件判断是为了防止多线程下cell覆盖的问题
                                 */
                                if ((rs = cells) != null &&
                                    (m = rs.length) > 0 &&
                                    rs[j = (m - 1) & h] == nu
                                    rs[j] = r; //将创建好的r赋值给当前位置
                                    created = true; //修改标志位 表示已经创建
                                }
                            } finally {
                                //锁状态还原
                                cellsBusy = 0;
                            }
                            //已经创建了,直接退出。
                            if (created)
                                break;
                            //否则继续循环。
                            continue;           // Slot is now non-empty
                        }
                    }
                    //扩容意向改为false。
                    collide = false;
                }
                
                //CASE1.2 只有一种情况wasUncontended为false,即当前cell不为null,
                //并且发生了竞争。 后续重置hash值
                else if (!wasUncontended)       
                    wasUncontended = true;     
				
                /*
                 * CASE1.3 当前线程rehash过hash值,然后新命中的cell不为空
                 *  执行: CAS操作更新cell中的数据,如果更新成功,就退出
                 *        更新失败,继续循环。
                 */
                else if (a.cas(v = a.value, ((fn == null) ? v + x :
                                             fn.applyAsLong(v, x))))
                    break;
                
                /*
                 *  CASE1.4 
                 *  n(cells数组长度)大于NCPU,或者已经被其他线程扩容过了,
                 *  那么当先线程rehash后重试即可
                 */
                else if (n >= NCPU || cells != as)
                    collide = false;   //扩容意向变为false
                
                /*
                 *  CASE 1.5
                 *  设置扩容意向为true。但是不一定发生扩容
                 */ 
                else if (!collide)
                    collide = true;
                                      
                /*
                 * CASE1.6
                 * 真正扩容的逻辑
                 * 条件1、cellsBusy == 0表示无锁状态,当前线程可以去竞争锁
                 * 条件2、casCellsBusy()CAS获取锁的逻辑,获取锁成功可以执行扩容逻辑,
                 *       false表示当前时刻有其他线程在做扩容相关的操作。
                 */
                else if (cellsBusy == 0 && casCellsBusy()) {
                    try {
                        //防止多线程下重复扩容
                        if (cells == as) {      
                            //扩容数组长度变为原来的2倍
                            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
                }
                
                //重置当前线程hash值
                h = advanceProbe(h);
            }
//----------------------------CASE1 END---------------------------------------
            
//----------------------------CASE2 Start-------------------------------------
            /*
             *  主要干的事:初始化cells数组
             *
             *  进入CASE2的前置条件 : cells数组还未初始化 as = cells = null
             *
             *  1、cellsBusy = 0 表示当前未加锁(初始化cells数组)
             *  2、as == cells 如果单线程的情况下没有必要,但是多线程情况下,如果有线程
             *   已经将cells初始化过了,此时as(null) != cells了,就进不来了。
             *  3、 casCellsBusy() 即获取锁成功(去初始化cells数组)。
             * 	
             */ 
            else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
                boolean init = false;
                try {      
                    // 还要判断一下 cells == as 也是防止其他线程已经初始化了cells,
                    // 导致当前线程又进行了初始化,造成了数据丢失。
                    if (cells == as) {
                        //初始化cell数组长度为2
                        Cell[] rs = new Cell[2];
                        //将x值赋给对应的Cell
                        rs[h & 1] = new Cell(x);
                        cells = rs; //赋值给cells
                        init = true;
                    }
                } finally {
                    cellsBusy = 0;
                }
                if (init)
                    break;
            }
//---------------------------CASE2 END----------------------------------------           
            
            
//--------------------------CASE3 Start--------------------------------------         	  
  /*
   *  1、当前cellsBusy加锁状态,表示其他线程正在初始化cells,所以当前线程将值累加到base中
   *  2、cells被其他线程初始化后,当前线程需要将数据累加到base、   	
   */   
                
            else if (casBase(v = base, ((fn == null) ? v + x :
                                        fn.applyAsLong(v, x))))
                break;                        
        }
    }

4.4sum()方法

   public long sum() {
        Cell[] as = cells; Cell a;
        //先将base中的值拿来
        long sum = base;
        //遍历Cells中每一个Cell,如果槽不为空,就累加Cell中的value值
        if (as != null) {
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null)
                    sum += a.value;
            }
        }
        //最终返回sum。
        return sum;
    }

5.总结

5.1AtomicLong可以弃用了吗

看上去LongAdder的性能全面超越了AtomicLong,而且阿里巴巴开发手册也提及到 推荐使用 LongAdder 对象,比 AtomicLong 性能更好(减少乐观锁的重试次数),但是我们真的就可以舍弃掉LongAdder了吗?

当然不是,我们需要看场景来使用,如果是并发不太高的系统,使用AtomicLong可能会更好一些,而且内存需求也会小一些。

我们看过sum()方法后可以知道LongAdder在统计的时候如果有并发更新,可能导致统计的数据有误差。

而在高并发统计计数的场景下,才更适合使用LongAdder

5.2收获

LongAdder中最核心的思想就是利用空间来换时间,将热点value分散成一个Cell列表来承接并发的CAS,以此来提升性能。

LongAdder的原理及实现都很简单,但其设计的思想值得我们品味和学习。

5.3参考

参考博客

参考路飞大佬博客

视频b站小刘老师

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

shstart7

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值