Map的并发处理(ConcurrentHashMap)
推薦相關文章:
http://blog.csdn.net/waitforcher/archive/2009/05/24/4211896.aspx
?
?
ConcurrentModificationException
在這種迭代方式中,當iterator被創建后集合再發生改變就不再是拋出ConcurrentModificationException, 取而代之的是在改變時new新的數據從而不影響原有的數據?,iterator完成后再將頭指針替換為新的數據?,這樣iterator線程可以使用原來老的數據,而寫線程也可以并發的完成改變。
?
ConcurrentHashMap 原理:
集合是編程中最常用的數據結構。而談到并發,幾乎總是離不開集合這類高級數據結構的支持。比如兩個線程需要同時訪問一個中間臨界區 (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代碼??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代碼???
?? remove操作非常類似put,但要注意一點區別,中間那個for循環是做什么用的呢?(*號標記)從代碼來看,就是將定位之后的所有entry克隆并拼回前面去,但有必要嗎?每次刪除一個元素就要將那之前的元素克隆一遍?這點其實是由entry 的不變性來決定的,仔細觀察entry定義,發現除了value,其他所有屬性都是用final來修飾的,這意味著在第一次設置了next域之后便不能再 改變它,取而代之的是將它之前的節點全都克隆一次。至于entry為什么要設置為不變性,這跟不變性的訪問不需要同步從而節省時間有關,關于不變性的更多 內容,請參閱之前的文章《線程高級---線程的一些編程技巧》
?
Java代碼???
Java代碼???
?
?
?
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 |
總結
以上是生活随笔為你收集整理的Map的并发处理(ConcurrentHashMap)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 维护100亿个URL
- 下一篇: map之erase