2013年05月30日 Onetwogoo 評論 33 條評論 37,900 人閱讀html
(本文由onetwogoo投稿)java
在《疫苗:Java HashMap的死循環》中,咱們看到,java.util.HashMap並不能直接應用於多線程環境。對於多線程環境中應用HashMap,主要有如下幾種選擇:git
而以上幾種方法在實現的具體細節上,都或多或少地用到了互斥鎖。互斥鎖會形成線程阻塞,下降運行效率,並有可能產生死鎖、優先級翻轉等一系列問題。github
CAS(Compare And Swap)是一種底層硬件提供的功能,它能夠將判斷並更改一個值的操做原子化。關於CAS的一些應用,《無鎖隊列的實現》一文中有很詳細的介紹。shell
在java.util.concurrent.atomic包中,Java爲咱們提供了不少方便的原子類型,它們底層徹底基於CAS操做。數組
例如咱們但願實現一個全局公用的計數器,那麼能夠:安全
1多線程 2性能 3atom 4 5 6 7 8 9 10 |
|
其中,compareAndSet方法會檢查counter現有的值是否爲oldValue,若是是,則將其設置爲新值newValue,操做成功並返回true;不然操做失敗並返回false。
當計算counter新值時,若其餘線程將counter的值改變,compareAndSwap就會失敗。此時咱們只需在外面加一層循環,不斷嘗試這個過程,那麼最終必定會成功將counter值+1。(其實AtomicInteger已經爲經常使用的+1/-1操做定義了incrementAndGet與decrementAndGet方法,之後咱們只需簡單調用它便可)
除了AtomicInteger外,java.util.concurrent.atomic包還提供了AtomicReference和AtomicReferenceArray類型,它們分別表明原子性的引用和原子性的引用數組(引用的數組)。
在實現無鎖HashMap以前,讓咱們先來看一下比較簡單的無鎖鏈表的實現方法。
以插入操做爲例:
但在操做中途,有可能其餘線程在A與B直接也插入了一些節點(假設爲D),若是咱們不作任何判斷,可能形成其餘線程插入節點的丟失。(見圖3)咱們能夠利用CAS操做,在爲節點A的next指針賦值時,判斷其是否仍然指向B,若是節點A的next指針發生了變化則重試整個插入操做。大體代碼以下:
1 2 3 4 5 6 7 8 |
|
(Node類的next字段爲AtomicReference<Node>類型,即指向Node類型的原子性引用)
無鎖鏈表的查找操做與普通鏈表沒有區別。而其刪除操做,則須要找到待刪除節點前方的節點A和後方的節點B,利用CAS操做驗證並更新節點A的next指針,使其指向節點B。
HashMap主要有插入、刪除、查找以及ReHash四種基本操做。一個典型的HashMap實現,會用到一個數組,數組的每項元素爲一個節點的鏈表。對於此鏈表,咱們能夠利用上文提到的操做方法,執行插入、刪除以及查找操做,但對於ReHash操做則比較困難。
如圖4,在ReHash過程當中,一個典型的操做是遍歷舊錶中的每一個節點,計算其在新表中的位置,而後將其移動至新表中。期間咱們須要操縱3次指針:
而這三次指針操做必須同時完成,才能保證移動操做的原子性。但咱們不難看出,CAS操做每次只能保證一個變量的值被原子性地驗證並更新,沒法知足同時驗證並更新三個指針的需求。
因而咱們不妨換一個思路,既然移動節點的操做如此困難,咱們可使全部節點始終保持有序狀態,從而避免了移動操做。在典型的HashMap實現中,數組的長度始終保持爲2i,而從Hash值映射爲數組下標的過程,只是簡單地對數組長度執行取模運算(即僅保留Hash二進制的後i位)。當ReHash時,數組長度加倍變爲2i+1,舊數組第j項鍊表中的每一個節點,要麼移動到新數組中第j項,要麼移動到新數組中第j+2i項,而它們的惟一區別在於Hash值第i+1位的不一樣(第i+1位爲0則仍爲第j項,不然爲第j+2i項)。
如圖5,咱們將全部節點按照Hash值的翻轉位序(如1101->1011)由小到大排列。當數組大小爲8時,二、18在一個組內;三、十一、27在另外一個組內。每組的開始,插入一個哨兵節點,以方便後續操做。爲了使哨兵節點正確排在組的最前方,咱們將正常節點Hash的最高位(翻轉後變爲最低位)置爲1,而哨兵節點不設置這一位。
當數組擴容至16時(見圖6),第二組分裂爲一個只含3的組和一個含有十一、27的組,但節點之間的相對順序並未改變。這樣在ReHash時,咱們就不須要移動節點了。
因爲擴容時數組的複製會佔用大量的時間,這裏咱們採用了將整個數組分塊,懶惰創建的方法。這樣,當訪問到某下標時,僅需判斷此下標所在塊是否已創建完畢(若是沒有則創建)。
另外定義size爲當前已使用的下標範圍,其初始值爲2,數組擴容時僅需將size加倍便可;定義count表明目前HashMap中包含的總節點個數(不算哨兵節點)。
初始時,數組中除第0項外,全部項都爲null。第0項指向一個僅有一個哨兵節點的鏈表,表明整條鏈的起點。初始時全貌見圖7,其中淺綠色表明當前未使用的下標範圍,虛線箭頭表明邏輯上存在,但實際未創建的塊。
初始化下標操做
數組中爲null的項都認爲處於未初始化狀態,初始化某個下標即表明創建其對應的哨兵節點。初始化是遞歸進行的,即若其父下標未初始化,則先初始化其父下標。(一個下標的父下標是其移除最高二進制位後獲得的下標)大體代碼以下:
1 2 3 4 5 6 7 8 9 10 11 |
|
其中getBucket即封裝過的獲取數組某下標內容的方法,setBucket同理。listInsert將從指定位置開始查找適合插入的位置插入給定的節點,若鏈表中已存在hash相同的節點則返回那個已存在的節點;不然返回新插入的節點。
插入操做
查找操做
刪除操做
《Split-Ordered Lists: Lock-Free Extensible Hash Tables》
(全文完)