專用於高併發的map類-----Map的併發處理(ConcurrentHashMap)

oncurrentModificationExceptionjava

在這種迭代方式中,當iterator被建立後集合再發生改變就再也不是拋出ConcurrentModificationException, 取而代之的是在改變時new新的數據從而不影響原有的數據 ,iterator完成後再將頭指針替換爲新的數據 ,這樣iterator線程可使用原來老的數據,而寫線程也能夠併發的完成改變。node

 

ConcurrentHashMap 原理:c++

集合是編程中最經常使用的數據結構。而談到併發,幾乎老是離不開集合這類高級數據結構的支持。好比兩個線程須要同時訪問一箇中間臨界區 (Queue),好比常會用緩存做爲外部文件的副本(HashMap)。這篇文章主要分析jdk1.5的3種併發集合類型 (concurrent,copyonright,queue)中的ConcurrentHashMap,讓咱們從原理上細緻的瞭解它們,可以讓咱們在深 度項目開發中獲益非淺。編程

    在tiger以前,咱們使用得最多的數據結構之一就是HashMap和Hashtable。你們都知道,HashMap中未進行同步考慮,而 Hashtable則使用了synchronized,帶來的直接影響就是可選擇,咱們能夠在單線程時使用HashMap提升效率,而多線程時用 Hashtable來保證安全。
    當咱們享受着jdk帶來的便利時一樣承受它帶來的不幸惡果。經過分析Hashtable就知道,synchronized是針對整張Hash表的,即每次 鎖住整張表讓線程獨佔,安全的背後是巨大的浪費,慧眼獨具的Doug Lee立馬拿出瞭解決方案----ConcurrentHashMap。
    ConcurrentHashMap和Hashtable主要區別就是圍繞着鎖的粒度以及如何鎖。如圖




    左邊即是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,併發是不可避免的啊!

 

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

 

Java代碼   收藏代碼
  1. static final class HashEntry {  
  2.     final K key;  
  3.     final int hash;  
  4.     volatile V value;  
  5.     final HashEntry next;  
  6.   
  7.     HashEntry(K key, int hash, HashEntry next, V value) {  
  8.         this.key = key;  
  9.         this.hash = hash;  
  10.         this.next = next;  
  11.         this.value = value;  
  12.     }  
  13. }  

 

 

 

 

 

ConcurrentHashMap 和 HashTable 的速度比較:
服務器

util.concurrent 包中的 ConcurrentHashMap 類(也將出如今JDK 1.5中的 java.util.concurrent 包中)是對 Map 的線程安全的實現,比起 synchronizedMap 來,它提供了好得多的併發性。多個讀操做幾乎總能夠併發地執行,同時進行的讀和寫操做一般也能併發地執行,而同時進行的寫操做仍然能夠不時地併發進行(相關的類也提供了相似的多個讀線程的併發性,可是,只容許有一個活動的寫線程) 。ConcurrentHashMap 被設計用來優化檢索操做;實際上,成功的 get() 操做完成以後一般根本不會有鎖着的資源。要在不使用鎖的狀況下取得線程安全性須要必定的技巧性,而且須要對Java內存模型(Java Memory Model)的細節有深刻的理解。ConcurrentHashMap 實現,加上 util.concurrent 包的其餘部分,已經被研究正確性和線程安全性的併發專家所正視。在下個月的文章中,咱們將看看 ConcurrentHashMap 的實現的細節。數據結構

ConcurrentHashMap 經過稍微地鬆弛它對調用者的承諾而得到了更高的併發性。檢索操做將能夠返回由最近完成的插入操做所插入的值,也能夠返回在步調上是併發的插入操做所添加的值(可是決不會返回一個沒有意義的結果)。由 ConcurrentHashMap.iterator() 返回的 Iterators 將每次最多返回一個元素,而且決不會拋出ConcurrentModificationException 異常,可是可能會也可能不會反映在該迭代器被構建以後發生的插入操做或者移除操做。在對 集合進行迭代時,不須要表範圍的鎖就能提供線程安全性。在任何不依賴於鎖整個表來防止更新的應用程序中,可使用 ConcurrentHashMap 來替代 synchronizedMap 或 Hashtable 。多線程

上述改進使得 ConcurrentHashMap 可以提供比 Hashtable 高得多的可伸縮性,並且,對於不少類型的公用案例(好比共享的cache)來講,還不用損失其效率。併發

好了多少?

表 1對 Hashtable 和  ConcurrentHashMap 的可伸縮性進行了粗略的比較。在每次運行過程當中, n 個線程併發地執行一個死循環,在這個死循環中這些線程從一個 Hashtable 或者 ConcurrentHashMap 中檢索隨機的key value,發如今執行 put() 操做時有80%的檢索失敗率,在執行操做時有1%的檢索成功率。測試所在的平臺是一個雙處理器的Xeon系統,操做系統是Linux。數據顯示了10,000,000次迭代以毫秒計的運行時間,這個數據是在將對 ConcurrentHashMap的 操做標準化爲一個線程的狀況下進行統計的。您能夠看到,當線程增長到多個時,ConcurrentHashMap 的性能仍然保持上升趨勢,而 Hashtable 的性能則隨着爭用鎖的狀況的出現而當即降了下來。

比起一般狀況下的服務器應用,此次測試中線程的數量看上去有點少。然而,由於每一個線程都在不停地對錶進行操做,因此這與實際環境下使用這個表的更多數量的線程的爭用狀況基本等同。

表 1.Hashtable 與 ConcurrentHashMap在可伸縮性方面的比較

線程數 ConcurrentHashMap Hashtable
1 1.00 1.03
2 2.59 32.40
4 5.58 78.23
8 13.21 163.48
16 27.58 341.21
32 57.27 778.41
相關文章
相關標籤/搜索