ConcurrentHashMap的鎖分離技術(源碼)

ConcurrentHashMap的鎖分離技術



 

 

concurrenthashmap是一個非常好的map實現,在高併發操作的場景下會有非常好的效率。實現的目的主要是爲了避免同步操作時對整個map對象進行鎖定從而提高併發訪問能力。

 

ConcurrentHashMap 類中包含兩個靜態內部類 HashEntry 和 Segment。HashEntry 用來封裝映射表的鍵 / 值對;Segment 用來充當鎖的角色,每個 Segment 對象守護整個散列映射表的若干個桶。每個桶是由若干個 HashEntry 對象鏈接起來的鏈表。一個 ConcurrentHashMap 實例中包含由若干個 Segment 對象組成的數組。

 

 

Java代碼 

 收藏代碼

  1. static final class HashEntry<K,V> {   
  2.        final K key;                       // 聲明 key 爲 final 型  
  3.        final int hash;                   // 聲明 hash 值爲 final 型   
  4.        volatile V value;                 // 聲明 value 爲 volatile 型  
  5.        final HashEntry<K,V> next;      // 聲明 next 爲 final 型   
  6.   
  7.        HashEntry(K key, int hash, HashEntry<K,V> next, V value) {   
  8.            this.key = key;   
  9.            this.hash = hash;   
  10.            this.next = next;   
  11.            this.value = value;   
  12.        }   
  13. }   

 

 

Java代碼 

 收藏代碼

  1. static final class Segment<K,V> extends ReentrantLock implements Serializable {   
  2.   
  3.         transient volatile int count;  //在本 segment 範圍內,包含的 HashEntry 元素的個數  
  4.                                     //volatile 型  
  5.   
  6.         transient int modCount;     //table 被更新的次數  
  7.   
  8.         transient int threshold;    //默認容量  
  9.   
  10.     final float loadFactor;    //裝載因子  
  11.   
  12.         /**  
  13.          * table 是由 HashEntry 對象組成的數組 
  14.          * 如果散列時發生碰撞,碰撞的 HashEntry 對象就以鏈表的形式鏈接成一個鏈表 
  15.          * table 數組的數組成員代表散列映射表的一個桶         
  16.          */   
  17.         transient volatile HashEntry<K,V>[] table;   
  18.         
  19.   
  20.         /**  
  21.          * 根據 key 的散列值,找到 table 中對應的那個桶(table 數組的某個數組成員) 
  22.      *     把散列值與 table 數組長度減 1 的值相“與”,得到散列值對應的 table 數組的下標 
  23.          *     然後返回 table 數組中此下標對應的 HashEntry 元素 
  24.      * 即這個段中鏈表的第一個元素 
  25.          */   
  26.         HashEntry<K,V> getFirst(int hash) {   
  27.             HashEntry<K,V>[] tab = table;               
  28.             return tab[hash & (tab.length - 1)];   
  29.         }   
  30.   
  31.   
  32.   
  33.         Segment(int initialCapacity, float lf) {   
  34.             loadFactor = lf;   
  35.             setTable(HashEntry.<K,V>newArray(initialCapacity));   
  36.         }   
  37.   
  38.         /**  
  39.          * 設置 table 引用到這個新生成的 HashEntry 數組 
  40.          * 只能在持有鎖或構造函數中調用本方法 
  41.          */   
  42.         void setTable(HashEntry<K,V>[] newTable) {               
  43.             threshold = (int)(newTable.length * loadFactor);   
  44.             table = newTable;   
  45.         }          
  46.  }   

 注意Segment繼承了ReentrantLock 鎖

 

左邊便是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能讀取到幾乎最新的數據,雖然可能不是最新的。要得到最新的數據,只有採用完全的同步。

Java代碼   收藏代碼

  1. V get(Object key, int hash) {  
  2.     if (count != 0) { // read-volatile  
  3.         HashEntry e = getFirst(hash);  
  4.         while (e != null) {  
  5.             if (e.hash == hash && key.equals(e.key)) {  
  6.                 V v = e.value;  
  7.                 if (v != null)  
  8.                     return v;  
  9.                 return readValueUnderLock(e); // recheck  
  10.             }  
  11.             e = e.next;  
  12.         }  
  13.     }  
  14.     return null;  
  15. }  
  16.   
  17.   
  18. V readValueUnderLock(HashEntry e) {  
  19.     lock();  
  20.     try {  
  21.         return e.value;  
  22.     } finally {  
  23.         unlock();  
  24.     }  
  25. }  

 

put操作一上來就鎖定了整個segment,這當然是爲了併發的安全,修改數據是不能併發進行的,必須得有個判斷是否超限的語句以確保容量不足時能夠rehash,而比較難懂的是這句int index = hash & (tab.length - 1),原來segment裏面纔是真正的hashtable,即每個segment是一個傳統意義上的hashtable,如上圖,從兩者的結構就可以看出區別,這裏就是找出需要的entry在table的哪一個位置,之後得到的entry就是這個鏈的第一個節點,如果e!=null,說明找到了,這是就要替換節點的值(onlyIfAbsent == false),否則,我們需要new一個entry,它的後繼是first,而讓tab[index]指向它,什麼意思呢?實際上就是將這個新entry插入到鏈頭,剩下的就非常容易理解了。

 

Java代碼   收藏代碼

  1. V put(K key, int hash, V value, boolean onlyIfAbsent) {  
  2.     lock();  
  3.     try {  
  4.         int c = count;  
  5.         if (c++ > threshold) // ensure capacity  
  6.             rehash();  
  7.         HashEntry[] tab = table;  
  8.         int index = hash & (tab.length - 1);  
  9.         HashEntry first = (HashEntry) tab[index];  
  10.         HashEntry e = first;  
  11.         while (e != null && (e.hash != hash || !key.equals(e.key)))  
  12.             e = e.next;  
  13.   
  14.         V oldValue;  
  15.         if (e != null) {  
  16.             oldValue = e.value;  
  17.             if (!onlyIfAbsent)  
  18.                 e.value = value;  
  19.         }  
  20.         else {  
  21.             oldValue = null;  
  22.             ++modCount;  
  23.             tab[index] = new HashEntry(key, hash, first, value);  
  24.             count = c; // write-volatile  
  25.         }  
  26.         return oldValue;  
  27.     } finally {  
  28.         unlock();  
  29.     }  
  30. }  

  

   remove操作非常類似put,但要注意一點區別,中間那個for循環是做什麼用的呢?(*號標記)從代碼來看,就是將定位之後的所有entry克隆並拼回前面去,但有必要嗎?每次刪除一個元素就要將那之前的元素克隆一遍?這點其實是由entry 的不變性來決定的,仔細觀察entry定義,發現除了value,其他所有屬性都是用final來修飾的,這意味着在第一次設置了next域之後便不能再 改變它,取而代之的是將它之前的節點全都克隆一次。至於entry爲什麼要設置爲不變性,這跟不變性的訪問不需要同步從而節省時間有關,關於不變性的更多 內容,請參閱之前的文章《線程高級---線程的一些編程技巧》

 

Java代碼   收藏代碼

  1. V remove(Object key, int hash, Object value) {  
  2.     lock();  
  3.     try {  
  4.         int c = count - 1;  
  5.         HashEntry[] tab = table;  
  6.         int index = hash & (tab.length - 1);  
  7.         HashEntry first = (HashEntry)tab[index];  
  8.         HashEntry e = first;  
  9.         while (e != null && (e.hash != hash || !key.equals(e.key)))  
  10.             e = e.next;  
  11.   
  12.         V oldValue = null;  
  13.         if (e != null) {  
  14.             V v = e.value;  
  15.             if (value == null || value.equals(v)) {  
  16.                 oldValue = v;  
  17.                 // All entries following removed node can stay  
  18.                 // in list, but all preceding ones need to be  
  19.                 // cloned.  
  20.                 ++modCount;  
  21.                 HashEntry newFirst = e.next;  
  22.             *    for (HashEntry p = first; p != e; p = p.next)  
  23.             *        newFirst = new HashEntry(p.key, p.hash,   
  24.                                                   newFirst, p.value);  
  25.                 tab[index] = newFirst;  
  26.                 count = c; // write-volatile  
  27.             }  
  28.         }  
  29.         return oldValue;  
  30.     } finally {  
  31.         unlock();  
  32.     }  
  33. }  

 

 

探索 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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章