ConcurrentHashMap之實現細節

jia635發表於2014-06-27



是Java 5中支援高併發、高吞吐量的執行緒安全HashMap實現。在這之前我對ConcurrentHashMap只有一些膚淺的理解,僅知道它採用了多個鎖,大概也足夠了。但是在經過一次慘痛的面試經歷之後,我覺得必須深入研究它的實現。面試中被問到讀是否要加鎖,因為讀寫會發生衝突,我說必須要加鎖,我和麵試官也因此發生了衝突,結果可想而知。還是閒話少說,通過仔細閱讀原始碼,現在總算理解ConcurrentHashMap實現機制了,其實現之精巧,令人歎服,與大家共享之。

實現原理 

鎖分離 (Lock Stripping)

ConcurrentHashMap允許多個修改操作併發進行,其關鍵在於使用了鎖分離技術。它使用了多個鎖來控制對hash表的不同部分進行的修改。ConcurrentHashMap內部使用段(Segment)來表示這些不同的部分,每個段其實就是一個小的hash table,它們有自己的鎖。只要多個修改操作發生在不同的段上,它們就可以併發進行。

 

有些方法需要跨段,比如size()和containsValue(),它們可能需要鎖定整個表而而不僅僅是某個段,這需要按順序鎖定所有段,操作完畢後,又按順序釋放所有段的鎖。這裡“按順序”是很重要的,否則極有可能出現死鎖,在ConcurrentHashMap內部,段陣列是final的,並且其成員變數實際上也是final的,但是,僅僅是將陣列宣告為final的並不保證陣列成員也是final的,這需要實現上的保證。這可以確保不會出現死鎖,因為獲得鎖的順序是固定的。不變性是多執行緒程式設計佔有很重要的地位,下面還要談到。


final Segment<K,V>[] segments;  

不變(Immutable)和易變(Volatile)

 

ConcurrentHashMap完全允許多個讀操作併發進行,讀操作並不需要加鎖。如果使用傳統的技術,如HashMap中的實現,如果允許可以在hash鏈的中間新增或刪除元素,讀操作不加鎖將得到不一致的資料。ConcurrentHashMap實現技術是保證HashEntry幾乎是不可變的。HashEntry代表每個hash鏈中的一個節點,其結構如下所示:

    static final class HashEntry<K,V> {
        final K key;
        final int hash;
        volatile V value;
        final HashEntry<K,V> next;
    }


可以看到除了value不是final的,其它值都是final的,這意味著不能從hash鏈的中間或尾部新增或刪除節點,因為這需要修改next引用值,所有的節點的修改只能從頭部開始。對於put操作,可以一律新增到Hash鏈的頭部。但是對於remove操作,可能需要從中間刪除一個節點,這就需要將要刪除節點的前面所有節點整個複製一遍,最後一個節點指向要刪除結點的下一個結點。這在講解刪除操作時還會詳述。為了確保讀操作能夠看到最新的值,將value設定成volatile,這避免了加鎖。

其它

為了加快定位段以及段中hash槽的速度,每個段hash槽的的個數都是2^n,這使得通過位運算就可以定位段和段中hash槽的位置。當併發級別為預設值16時,也就是段的個數,hash值的高4位決定分配在哪個段中。但是我們也不要忘記《演算法導論》給我們的教訓:hash槽的的個數不應該是2^n,這可能導致hash槽分配不均,這需要對hash值重新再hash一次。(這段似乎有點多餘了 )

這是重新hash的演算法,還比較複雜


private static int hash(int h) { 

// Spread bits to regularize both segment and index locations,  

 // using variant of single-word Wang/Jenkins hash.

 h += (h <<  15) ^ 0xffffcd7d;  

 h ^= (h >>> 10);  

   h += (h <<   3);  

 h ^= (h >>>  6);  

h += (h <<   2) + (h << 14); 

 return h ^ (h >>> 16);  


這是定位段的方法:

    final Segment<K,V> segmentFor(int hash) {
        return segments[(hash >>> segmentShift) & segmentMask];
    }

資料結構

 

關於Hash表的基礎資料結構,這裡不想做過多的探討。Hash表的一個很重要方面就是如何解決hash衝突,ConcurrentHashMap和HashMap使用相同的方式,都是將hash值相同的節點放在一個hash鏈中。與HashMap不同的是,ConcurrentHashMap使用多個子Hash表,也就是段(Segment)。下面是ConcurrentHashMap的資料成員:


ConcurrentHashMap 類中包含兩個靜態內部類 HashEntry 和 Segment。HashEntry 用來封裝對映表的鍵 / 值對;Segment 用來充當鎖的角色,每個 Segment 物件守護整個雜湊對映表的若干個桶。每個桶是由若干個 HashEntry 物件連結起來的連結串列。一個 ConcurrentHashMap 例項中包含由若干個 Segment 物件組成的陣列。

 static final class HashEntry<K,V> { 
        final K key;                       // 宣告 key 為 final 型
        final int hash;                   // 宣告 hash 值為 final 型 
        volatile V value;                 // 宣告 value 為 volatile 型
        final HashEntry<K,V> next;      // 宣告 next 為 final 型 


        HashEntry(K key, int hash, HashEntry<K,V> next, V value) { 
            this.key = key; 
            this.hash = hash; 
            this.next = next; 
            this.value = value; 
        } 
 } 


static final class Segment<K,V> extends ReentrantLock implements Serializable { 


        transient volatile int count;  //在本 segment 範圍內,包含的 HashEntry 元素的個數
                   //volatile 型


        transient int modCount;     //table 被更新的次數


        transient int threshold;    //預設容量


final float loadFactor;    //裝載因子


        /** 
         * table 是由 HashEntry 物件組成的陣列
         * 如果雜湊時發生碰撞,碰撞的 HashEntry 物件就以連結串列的形式連結成一個連結串列
         * table 陣列的陣列成員代表雜湊對映表的一個桶        
         */ 
        transient volatile HashEntry<K,V>[] table; 
      


        /** 
         * 根據 key 的雜湊值,找到 table 中對應的那個桶(table 陣列的某個陣列成員)
*     把雜湊值與 table 陣列長度減 1 的值相“與”,得到雜湊值對應的 table 陣列的下標
         *     然後返回 table 陣列中此下標對應的 HashEntry 元素
* 即這個段中連結串列的第一個元素
         */ 
        HashEntry<K,V> getFirst(int hash) { 
            HashEntry<K,V>[] tab = table;             
            return tab[hash & (tab.length - 1)]; 
        } 






        Segment(int initialCapacity, float lf) { 
            loadFactor = lf; 
            setTable(HashEntry.<K,V>newArray(initialCapacity)); 
        } 


        /** 
         * 設定 table 引用到這個新生成的 HashEntry 陣列
         * 只能在持有鎖或建構函式中呼叫本方法
         */ 
        void setTable(HashEntry<K,V>[] newTable) {             
            threshold = (int)(newTable.length * loadFactor); 
            table = newTable; 
        }        
 } 

左邊便是Hashtable的實現方式---鎖整個hash表;而右邊則是ConcurrentHashMap的實現方式---鎖桶(或段)。 ConcurrentHashMap將hash表分為16個桶(預設值),諸如get,put,remove等常用操作只鎖當前需要用到的桶。試想,原來 只能一個執行緒進入,現在卻能同時16個寫執行緒進入(寫執行緒才需要鎖定,而讀執行緒幾乎不受限制,之後會提到),併發性的提升是顯而易見的。
    更令人驚訝的是ConcurrentHashMap的讀取併發,因為在讀取的大多數時候都沒有用到鎖定,所以讀取操作幾乎是完全的併發操作,而寫操作鎖定的粒度又非常細,比起之前又更加快速(這一點在桶更多時表現得更明顯些)。只有在求size等操作時才需要鎖定整個表。而在迭代時,ConcurrentHashMap使用了不同於傳統集合的快速失敗迭代器(見之前的文章《JAVA API備忘---集合》)的另一種迭代方式,我們稱為弱一致迭代器。在這種迭代方式中,當iterator被建立後集合再發生改變就不再是丟擲ConcurrentModificationException,取而代之的是在改變時new新的資料從而不影響原有的資料,iterator完成後再將頭指標替換為新的資料,這樣iterator執行緒可以使用原來老的資料,而寫執行緒也可以併發的完成改變,更重要的,這保證了多個執行緒併發執行的連續性和擴充套件性,是效能提升的關鍵。
    接下來,讓我們看看ConcurrentHashMap中的幾個重要方法,心裡知道了實現機制後,使用起來就更加有底氣。

    ConcurrentHashMap中主要實體類就是三個:ConcurrentHashMap(整個Hash表),Segment(桶),HashEntry(節點),對應上面的圖可以看出之間的關係。
    get方法(請注意,這裡分析的方法都是針對桶的,因為ConcurrentHashMap的最大改進就是將粒度細化到了桶上),首先判斷了當前桶的資料 個數是否為0,為0自然不可能get到什麼,只有返回null,這樣做避免了不必要的搜尋,也用最小的代價避免出錯。然後得到頭節點(方法將在下面涉及) 之後就是根據hash和key逐個判斷是否是指定的值,如果是並且值非空就說明找到了,直接返回;程式非常簡單,但有一個令人困惑的地方,這句 return readValueUnderLock(e)到底是用來幹什麼的呢?研究它的程式碼,在鎖定之後返回一個值。但這裡已經有一句V v = e.value得到了節點的值,這句return readValueUnderLock(e)是否多此一舉?事實上,這裡完全是為了併發考慮的,這裡當v為空時,可能是一個執行緒正在改變節點,而之前的get操作都未進行鎖定,根據bernstein條件,讀後寫或寫後讀都會引起資料的不一致,所以這裡要對這個e重新上鎖再讀一遍,以保證得到的是正確值,這裡不得不佩服Doug Lee思維的嚴密性。整個get操作只有很少的情況會鎖定,相對於之前的Hashtable,併發是不可避免的啊!

 

get操作不需要鎖。第一步是訪問count變數,這是一個volatile變數,由於所有的修改操作在進行結構修改時都會在最後一步寫count變數,通過這種機制保證get操作能夠得到幾乎最新的結構更新。對於非結構更新,也就是結點值的改變,由於HashEntry的value變數是volatile的,也能保證讀取到最新的值。接下來就是對hash鏈進行遍歷找到要獲取的結點,如果沒有找到,直接訪回null。對hash鏈進行遍歷不需要加鎖的原因在於鏈指標next是final的。但是頭指標卻不是final的,這是通過getFirst(hash)方法返回,也就是存在table陣列中的值。這使得getFirst(hash)可能返回過時的頭結點,例如,當執行get方法時,剛執行完getFirst(hash)之後,另一個執行緒執行了刪除操作並更新頭結點,這就導致get方法中返回的頭結點不是最新的。這是可以允許,通過對count變數的協調機制,get能讀取到幾乎最新的資料,雖然可能不是最新的。要得到最新的資料,只有採用完全的同步。


探索 ConcurrentHashMap 高併發性的實現機制:
http://www.ibm.com/developerworks/cn/java/java-lo-concurrenthashmap/


 

 

 

ConcurrentHashMap之實現細節
http://www.iteye.com/topic/344876


Map的併發處理(ConcurrentHashMap)

http://zl198751.iteye.com/blog/907927

 

集合框架 Map篇(4)----ConcurrentHashMap

http://hi.baidu.com/yao1111yao/blog/item/232f2dfc55fbcd5ad7887d9f.html

 

 

java ConcurrentHashMap中的一點點迷惑

http://icanfly.iteye.com/blog/1450165



相關文章