從LongAdder看更高效的無鎖實現

接觸到AtomicLong的原因是在看guava的LoadingCache相關代碼時,關于LoadingCache,其實思路也非常簡單清晰:用模板模式解決了緩存不命中時獲取數據的邏輯,這個思路我早前也正好在項目中使用到。

言歸正傳,為什么說LongAdder引起了我的注意,原因有二:

  1. 作者是Doug lea ,地位實在舉足輕重。

  2. 他說這個比AtomicLong高效。

我們知道,AtomicLong已經是非常好的解決方案了,涉及并發的地方都是使用CAS操作,在硬件層次上去做 compare and set操作。效率非常高。

因此,我決定研究下,為什么LongAdder比AtomicLong高效。

首先,看LongAdder的繼承樹:

1.png

繼承自Striped64,這個類包裝了一些很重要的內部類和操作。稍候會看到。

正式開始前,強調下,我們知道,AtomicLong的實現方式是內部有個value 變量,當多線程并發自增,自減時,均通過CAS 指令從機器指令級別操作保證并發的原子性。

再看看LongAdder的方法:

2.png

怪不得可以和AtomicLong作比較,連API都這么像。我們隨便挑一個API入手分析,這個API通了,其他API都大同小異,因此,我選擇了add這個方法。事實上,其他API也都依賴這個方法。

3.png

LongAdder中包含了一個Cell 數組,Cell是Striped64的一個內部類,顧名思義,Cell 代表了一個最小單元,這個單元有什么用,稍候會說道。先看定義:

4.png

Cell內部有一個非常重要的value變量,并且提供了一個CAS更新其值的方法。

回到add方法:

5.png

這里,我有個疑問,AtomicLong已經使用CAS指令,非常高效了(比起各種鎖),LongAdder如果還是用CAS指令更新值,怎么可能比AtomicLong高效了? 何況內部還這么多判斷?。?!

這是我開始時最大的疑問,所以,我猜想,難道有比CAS指令更高效的方式出現了? 帶著這個疑問,繼續。

第一if 判斷,第一次調用的時候cells數組肯定為null,因此,進入casBase方法:

6.png原子更新base沒啥好說的,如果更新成功,本地調用開始返回,否則進入分支內部。

什么時候會更新失?。?沒錯,并發的時候,好戲開始了,AtomicLong的處理方式是死循環嘗試更新,直到成功才返回,而LongAdder則是進入這個分支。

分支內部,通過一個Threadlocal變量threadHashCode 獲取一個HashCode對象,該HashCode對象依然是Striped64類的內部類,看定義:

7.png

有個code變量,保存了一個非0的隨機數隨機值。

回到add方法:

7.png

拿到該線程相關的HashCode對象后,獲取它的code變量,as[(n-1)&h] 這句話相當于對h取模,只不過比起取模,因為是 與 的運算所以效率更高。

計算出一個在Cells 數組中當先線程的HashCode對應的 索引位置,并將該位置的Cell 對象拿出來用CAS更新它的value值。

當然,如果as 為null 并且更新失敗,才會進入retryUpdate方法。

看到這里我想應該有很多人明白為什么LongAdder會比AtomicLong更高效了,沒錯,唯一會制約AtomicLong高效的原因是高并發,高并發意味著CAS的失敗幾率更高, 重試次數更多,越多線程重試,CAS失敗幾率又越高,變成惡性循環,AtomicLong效率降低。 那怎么解決? LongAdder給了我們一個非常容易想到的解決方案:減少并發,將單一value的更新壓力分擔到多個value中去,降低單個value的 “熱度”,分段更新?。?!

這樣,線程數再多也會分擔到多個value上去更新,只需要增加value就可以降低 value的 “熱度”  AtomicLong中的 惡性循環不就解決了嗎? cells 就是這個 “段” cell中的value 就是存放更新值的, 這樣,當我需要總數時,把cells 中的value都累加一下不就可以了么??!

當然,聰明之處遠遠不僅僅這里,在看看add方法中的代碼,casBase方法可不可以不要,直接分段更新,上來就計算 索引位置,然后更新value?

答案是不好,不是不行,因為,casBase操作等價于AtomicLong中的CAS操作,要知道,LongAdder這樣的處理方式是有壞處的,分段操作必然帶來空間上的浪費,可以空間換時間,但是,能不換就不換,看空間時間都節約~! 所以,casBase操作保證了在低并發時,不會立即進入分支做分段更新操作,因為低并發時,casBase操作基本都會成功,只有并發高到一定程度了,才會進入分支,所以,Doug Lea對該類的說明是: 低并發時LongAdder和AtomicLong性能差不多,高并發時LongAdder更高效!

9.png

但是,Doung Lea 還是沒這么簡單,聰明之處還沒有結束……

如此,retryUpdate中做了什么事,也基本略知一二了,因為cell中的value都更新失敗(說明該索引到這個cell的線程也很多,并發也很高時) 或者cells數組為空時才會調用retryUpdate,

因此,retryUpdate里面應該會做兩件事:

  1. 擴容,將cells數組擴大,降低每個cell的并發量,同樣,這也意味著cells數組的rehash動作。

  2.  給空的cells變量賦一個新的Cell數組

是不是這樣呢? 繼續看代碼:

      boolean created = false;
                          try {               // Recheck under lock
                              Cell[] rs; int m, j;
                              if ((rs = cells) != null &&
                                      (m = rs.length) > 0 &&
                                      rs[j = (m - 1) & h] == null) {
                                  rs[j] = r;
                                  created = true;
                              }
                          } finally {
                              busy = 0;
                          }
                          if (created)
                              break;
                          continue;           // Slot is now non-empty
                      }
                  }
                  collide = false;
              }
              else if (!wasUncontended)       // CAS already known to fail
                  wasUncontended = true;      // Continue after rehash
              else if (a.cas(v = a.value, fn(v, x)))
                  break;
              else if (n >= NCPU || cells != as)
                  collide = false;            // At max size or stale
              else if (!collide)
                  collide = true;
              else if (busy == 0 && casBusy()) {
                  try {
                      if (cells == as) {      // Expand table unless stale
                          Cell[] rs = new Cell[n << 1];
                          for (int i = 0; i < n; ++i)
                              rs[i] = as[i];
                          cells = rs;
                      }
                  } finally {
                      busy = 0;
                  }
                  collide = false;
                  continue;                   // Retry with expanded table
              }
              h ^= h << 13;                   // Rehash  h ^= h >>> 17;
              h ^= h << 5;
          }
          else if (busy == 0 && cells == as && casBusy()) {//分支2
              boolean init = false;
              try {                           // Initialize table
                  if (cells == as) {
                      Cell[] rs = new Cell[2];
                      rs[h & 1] = new Cell(x);
                      cells = rs;
                      init = true;
                  }
              } finally {
                  busy = 0;
              }
              if (init)
                  break;
          }
          else if (casBase(v = base, fn(v, x)))
              break;                          // Fall back on using base
      }
      hc.code = h;                            // Record index for next time
  }

代碼比較長,變成文本看看,為了方便大家看if else 分支,對應的  { } 我用相同的顏色標注出來。可以看到,這個時候Doug Lea才愿意使用死循環保證更新成功~!分支2中,為cells為空的情況,需要new 一個Cell數組。

分支1分支中,略復雜一點點:

注意,幾個分支中都提到了busy這個方法,這個可以理解為一個CAS實現的鎖,只有在需要更新cells數組的時候才會更新該值為1,如果更新失敗,則說明當前有線程在更新cells數組,當前線程需要等待。重試。

回到分支1中,這里首先判斷當前cells數組中的索引位置的cell元素是否為空,如果為空,則添加一個cell到數組中。

否則更新 標示沖突的標志位wasUncontended 為 true ,重試。

否則,再次更新cell中的value,如果失敗,重試。

。。。。。。。一系列的判斷后,如果還是失敗,下下下策,reHash,直接將cells數組擴容一倍,并更新當前線程的hash值,保證下次更新能盡可能成功。

可以看到,LongAdder確實用了很多心思減少并發量,并且,每一步都是在”沒有更好的辦法“的時候才會選擇更大開銷的操作,從而盡可能的用最最簡單的辦法去完成操作。追求簡單,但是絕對不粗暴。

———————陳皓注————————

最后留給大家思考的兩個問題:

1)是不是AtomicLong可以被廢了?

2)如果cell被創建后,原來的casBase就不走了,會不會性能更差?

———————liuinsect注————————

昨天和左耳朵耗子簡單討論了下,發現左耳朵耗子,耗哥對讀者思維的引導還是非常不錯的,在第一次發現這個類后,對里面的實現又提出了更多的問題,引導大家思考,值得學習。

我們 發現的問題有這么幾個(包括以上的問題),自己簡單總結下,歡迎大家討論:

1. jdk 1.7中是不是有這個類?
我確認后,結果如下:    jdk-7u51 版本上還沒有  但是jdk-8u20版本上已經有了。代碼基本一樣 ,增加了對double類型的支持和刪除了一些冗余的代碼。有興趣的同學可以去下載下JDK 1.8看看

2. base有沒有參與匯總?
base在調用intValue等方法的時候是會匯總的:

10.bmp

3. 如果cell被創建后,原來的casBase就不走了,會不會性能更差? base的順序可不可以調換?
    剛開始我想可不可以調換add方法中的判斷順序,比如,先做casBase的判斷? 仔細思考后認為還是 不調換可能更好,調換后每次都要CAS一下,在高并發時,失敗幾率非常高,并且是惡性循環,比起一次判斷,后者的開銷明顯小很多,還沒有副作用(上一個問題,base變量在sum時base是會被統計的,并不會丟掉base的值)。因此,不調換可能會更好。

4. AtomicLong可不可以廢掉?
我的想法是可以廢掉了,因為,雖然LongAdder在空間上占用略大,但是,它的性能已經足以說明一切了,無論是從節約空的角度還是執行效率上,AtomicLong基本沒有優勢了,具體看這個測試(感謝Lemon的回復):http://blog.palominolabs.com/2014/02/10/java-8-performance-improvements-longadder-vs-atomiclong/

轉自http://coolshell.cn/articles/11454.html

原創文章,作者:s19930811,如若轉載,請注明出處:http://www.www58058.com/2091

(0)
s19930811s19930811
上一篇 2016-06-01 15:37
下一篇 2016-06-01

相關推薦

  • 軟件包管理、自建yum源與LAMP架構的自動編譯安裝

    軟件包管理 CentOS采用RedHat開發的rpm包管理器管理應用程序包。rpm包是由二進制可執行程序、庫、配置文件、幫助文件等組成,支持安裝、卸載、查詢、升級、降級、校驗等操作。 從組成結構上,rpm包由文件清單、安裝和卸載時運行的腳本構成。 包管理器有其自帶的公共數據庫。其數據包括:程序包的名稱、版本、依賴關系,功能說明,及各個文件的路徑及校驗碼信息等…

    Linux干貨 2016-12-05
  • Linux自動備份腳本

    原創作品,允許轉載,轉載時請務必以超鏈接形式標明文章 原始出處 、作者信息和本聲明。否則將追究法律責任。http://nolinux.blog.51cto.com/4824967/1541163        今天網上一個朋友問了我一個shell的題目,讓我幫他做下。下面是題目以及解題思路。 題目:…

    Linux干貨 2016-08-15
  • Lua簡明教程

    這幾天系統地學習了一下Lua這個腳本語言,Lua腳本是一個很輕量級的腳本,也是號稱性能最高的腳本,用在很多需要性能的地方,比如:游戲腳本,nginx,wireshark的腳本,當你把他的源碼下下來編譯后,你會發現解釋器居然不到200k,這是多么地變態啊(/bin/sh都要1M,MacOS平臺),而且能和C語言非常好的互動。我很好奇得瀏覽了一下Lua解釋器的源…

    Linux干貨 2016-08-15
  • 馬哥教育網絡20期+第4周作業博客

    1、復制/etc/skel目錄為/home/tuser1,要求/home/tuser1及其內部文件的屬組和其它用戶均沒有任何訪問權限。 答:cp -a /etc/ske1 /home/tuser1 && chown -R go-rwx /home/tuser1 2、編輯/etc/group文件,添加組hadoop。 答:echo "…

    Linux干貨 2016-09-05
  • vim文本編輯器之快捷鍵滿天飛

    vim文本編輯器不同于nano的是其功能非常強大,強大的功能還支持各種快捷鍵,讓我們編輯文本的時候更方便更快捷。 本文將會按照下圖所展示的功能來對命令一一講解,           打開文件:       &n…

    Linux干貨 2016-08-11
  • MySQL/MariaDB基于MMM實現讀寫分離及高可用

    前言 MMM(Master-Master replication managerfor Mysql,Mysql主主復制管理器)是一套靈活的腳本程序,基于perl實現,用來對mysql replication進行監控和故障遷移,并能管理mysql Master-Master復制的配置(同一時間只有一個節點是可寫的)。 MMM 優缺點 優點:高可用性,擴展性好,…

    Linux干貨 2015-06-24
欧美性久久久久