LongAdder解析

 对LongAdder的最初了解是从Coolshell上的一篇文章中获得的,但是一直都没有深入的了解过其实现,只知道它相较于AtomicLong来说,更加适合读多写少的并发情景。今天,我们就研究一下LongAdder的原理,探究一下它如此高效的原因。

基本原理和思想

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

Add操作

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
    }
}

cellsLongAdder的父类Striped64中的Cell数组类型的成员变量。每个Cell对象中都包含一个value值,并提供对这个value值的CAS操作。
 我们来看一下casBase函数相关的源码吧。我们可以认为变量base就是第一个value值,也是基础value变量。先调用casBase函数来cas一下base变量,如果成功了,就不需要在进行下面比较复杂的算法,

    final boolean casBase(long cmp, long val) {
        return UNSAFE.compareAndSwapLong(this, BASE, cmp, val);
    }

然后我们继续看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方法。

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) { // 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
       }
   }

后记

 本篇文章写的不是很好,我写完之后又看了一遍coolshell上的这篇关于LongAdder文章,感觉自己没有人家写的那么简介明了。我对代码细节的注释和投入太多了。其实很多代码大家都可以看懂,并不需要大量的代码片段加注释。以后要注意一下。之后会接着研究一下JUC包中的其他类,希望大家多多关注。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352

推荐阅读更多精彩内容

  • iOS网络架构讨论梳理整理中。。。 其实如果没有APIManager这一层是没法使用delegate的,毕竟多个单...
    yhtang阅读 5,184评论 1 23
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,233评论 11 349
  • 一、基本数据类型 注释 单行注释:// 区域注释:/* */ 文档注释:/** */ 数值 对于byte类型而言...
    龙猫小爷阅读 4,258评论 0 16
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,620评论 18 399
  • Java8张图 11、字符串不变性 12、equals()方法、hashCode()方法的区别 13、...
    Miley_MOJIE阅读 3,699评论 0 11