当前位置: 首页 > 科技观察

LongAdder,这哥们好厉害

时间:2023-03-12 00:54:18 科技观察

上一篇我们介绍了AtomicLong。如果你还不知道,我建议你阅读这篇文章。AtomicXXX的神奇之旅为什么要先说AtomicLong呢?因为LongAdder的设计是基于AtomicLong的缺陷。为什么引入LongAdder我们知道AtomicLong是利用底层的CAS操作来提供并发的,比如addAndGet方法publicfinallongaddAndGet(longdelta){returnunsafe.getAndAddLong(this,valueOffset,delta)+delta;}我们也知道CAS是一个轻量级的spin方法,其逻辑是利用spin方法不断更新目标值,直到更新成功。(也就是乐观锁的实现方式)在并发数比较低的场景下,线程冲突的概率比较小,自旋的次数也不会很多。但是在并发数激增的情况下,会出现大量的失败和不断自旋的场景。这时候AtomicLong的自旋数很容易成为瓶颈。为了解决这个缺陷,引入了本文的主角---LongAdder,主要解决了AtomicLong在高并发环境下的自旋瓶颈问题。要了解LongAdder,我们先看看JDK源码中关于LongAdder的讨论。这段话很清楚的说明,在多线程环境下,如果业务场景更侧重于统计或者信息收集,LongAdder比AtomicLong有更好的性能。更高的吞吐量,但更多的内存使用。在低并发或单线程场景下,AtomicLong和LongAdder有相同的特点,即在上述场景下,AtomicLong和LongAdder可以互换。首先我们看一下LongAdder类的定义:可以看到LongAdder继承自Striped64类,实现了Serializable接口。Striped64继承自Number类。现在您可能不知道Striped64的作用,但不用担心。我们知道Number类是基本数据类型的包装类,是原子包装类的父类。继承Number类意味着可以将其视为数值。然而,这并不能说明什么。它仍然让我们感到困惑。然后查看了Striped64的继承体系,发现这个类有几个实现,这些实现可以帮助我们理解Striped64是什么。在里面我们看到了Accumulator,它的中文概念是累加器,所以我们猜测Striped64也可以实现累加功能。果然,翻了一些帖子求助后,可以得出一个结论:Striped64是一个64位的累加器。但是那两个Accumulators的实现是可以实现累加的,这个很好理解,但是这两个Adders呢?他们也能实现累加功能吗?我们仔细看看LongAdder和DoubleAdder的源码(因为这两个类很相似)。(下面的叙述风格主要以LongAdder为主,聊天也会用到DoubleAdder,但不是主要叙述对象,毕竟这两个类的基类很相似)看完一波源码代码中,我们发现了一些方法,例如increment、decrement、add、reset、sum等,这些方法好像是一个累加器拥有的函数。详细看了源码后,验证了我的想法,因为自增和自减都调用了add方法,add方法底层使用的是longAcculate。我给你画个图你就知道了。因此,LongAdder的第一个特点就是可以处理值,实现Serializable接口意味着LongAdder可以序列化存储在文件中或通过网络传输。然后我们继续往下走,现在时间的罗盘指向了LongAdder的add方法。我们脑海中已经有了这个LongAdder方法是并发累加的印象,那么如果不出意外的话,这个add方法是并发累加的方法,那就很奇怪了,为什么这个方法不用synchronized呢?我们继续观察add方法做了哪些操作。一开始我们只是分配了一些变量,没什么特别的。继续看第二行的时候,发现有一个cells对象,是一个数组,好像是全局的。.....那么它是在哪里定义的呢?想到有一个Striped64类,果断点进去,看到了这个全局变量。另外,Striped64这个类很重要,大家一定不要忘记。在Striped64类中,赫然出现了以下三个重要的全局变量。这三个被transient修饰的全局变量保证不会被序列化。先不解释这三个变量是什么,有什么用,因为刚到add方法,所以我们回到add方法:继续往下,我们看到caseBase方法,这个方法是干什么的?点进去,发现还是Striped64中的一个方法。现在,我们陷入了沉思。我们只知道Striped64是一个累加器,但我现在是否应该阅读Striped64的源代码?是时候好好介绍一下了。cells它是一个数组,如果不为空,则其大小为2的幂。base是Striped64的基础值。主要在没有争用的情况下使用。当使用CAS初始化和更新单元格时也会使用它。cellsBusy是一个SpinLock(自旋锁),在初始化cells的时候用到。除了这三个变量,还有一个变量,我忽略了,sinsin。它代表CPU的核心数。好像没什么玄机,再往下走这里,有一个Cell元素的内部类,里面包含了一个longvalue值,cas方法,UNSAFE,valueOffSet元素,如果你看过我的文章Atomic原子的话你就明白了AtomicLong的设计或者知道AtomicLong的设计,就会知道Cell的设计和AtomicLong的设计是一模一样的,都是使用volatile变量,Unsafe加上字段偏移量,然后使用CAS修改。而这个Cell元素就是cells数组中的每个对象。这里比较特别的是@sun.misc.Contended注解,这是Java8中的一个新注解,用来避免缓存的错误共享,减少CPU缓存级别的竞争。害,就这些?不用担心,Striped64的核心功能是分别为LongAdder和DoubleAdder提供并发累加函数,所以Striped64中的longAccumulate和doubleAccumulate是关键。我们主要介绍longAccumulate方法。方法比较长,我们慢慢进入节奏。最关键的longAccumulate先贴出longAccumulate的完整代码,接下来我们分析一下:finalvoidlongAccumulate(longx,LongBinaryOperatorfn,booleanwasUncontended){//获取thread的hash值inth;if((h=getProbe())==0){ThreadLocalRandom.current();//forceinitializationh=getProbe();wasUncontended=true;}booleancollide=false;//Trueiflastslotnonemptyfor(;;){Cell[]as;Cella;intn;longv;if((as=cells)!=null&&(n=as.length)>0){//cells已经初始化if((a=as[(n-1)&h])==null){//对应的cell不存在且需要创建if(cellsBusy==0){//尝试创建一个新的Cellr=newCell(x);if(cellsBusy==0&&casCellsBusy()){//lockbooleancreated=false;try{//上锁后,判断cells对应元素是否被占用Cell[]rs;intm,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)//cell创建完毕,可以退出break;continue;//加锁后发现单元格元素is不再为Empty,poll重试}}collide=false;}//下面的else是在尝试检测当前竞争是否过高,如果大就尝试扩容,if//如果扩容没用,tryrehash分散并发转到不同的cellelseif(!wasUncontended)//已知CAS失败,说明并发高。wasUncontended=true;//rehash后重试elseif(a.cas(v=a.value,((fn==null)?v+x://尝试CAS更新单元格的值fn.applyAsLong(v,x))))break;elseif(n>=NCPU||cells!=as)//cells数组已经够大了,rehashcollide=false;//Atmaxsizeorstaleelseif(!collide)//这说明其他竞争已经很大了,rehashcollide=true;elseif(cellsBusy==0&&casCellsBusy()){//rehash没用,尝试扩容try{if(cells==as){//加锁过程中可能有其他线程扩容,需要排除Cell[]rs=newCell[n<<1];for(inti=0;i