曾經(jīng)研究過jkd1.5新特性,其中ConcurrentHashMap就是其中之一,其特點:效率比Hashtable高,并發(fā)性比hashmap好。結(jié)合了兩者的特點。 集合是編程中最常用的數(shù)據(jù)結(jié)構。而談到并發(fā),幾乎總是離不開集合這類高級數(shù)據(jù)結(jié)構的支持。比如兩個線程需要同時訪問一個中間臨界區(qū)(Queue),比如常會用緩存作為外部文件的副本(HashMap)。這篇文章主要分析jdk1.5的3種并發(fā)集合類型(concurrent,copyonright,queue)中的ConcurrentHashMap,讓我們從原理上細致的了解它們,能夠讓我們在深度項目開發(fā)中獲益非淺。
在tiger之前,我們使用得最多的數(shù)據(jù)結(jié)構之一就是 HashMap和Hashtable。大家都知道,HashMap中未進行同步考慮,而Hashtable則使用了synchronized,帶來的直接影響就是可選擇,我們可以在單線程時使用HashMap提高效率,而多線程時用Hashtable來保證安全。
當我們享受著jdk帶來的便利時同樣承受它帶來的不幸惡果。通過分析Hashtable就知道,synchronized是針對整張Hash表的,即每次鎖住整張表讓線程獨占,安全的背后是巨大的浪費,慧眼獨具的Doug Lee立馬拿出了解決方案----ConcurrentHashMap。
ConcurrentHashMap和Hashtable主要區(qū)別就是圍繞著鎖的粒度以及如何鎖。如圖

左邊便是Hashtable的實現(xiàn)方式---鎖整個hash表;而右邊則是ConcurrentHashMap的實現(xiàn)方式---鎖桶(或段)。 ConcurrentHashMap將hash表分為16個桶(默認值),諸如get,put,remove等常用操作只鎖當前需要用到的桶。試想,原來只能一個線程進入,現(xiàn)在卻能同時16個寫線程進入(寫線程才需要鎖定,而讀線程幾乎不受限制,之后會提到),并發(fā)性的提升是顯而易見的。
更令人驚訝的是ConcurrentHashMap的讀取并發(fā),因為在讀取的大多數(shù)時候都沒有用到鎖定,所以讀取操作幾乎是完全的并發(fā)操作,而寫操作鎖定的粒度又非常細,比起之前又更加快速(這一點在桶更多時表現(xiàn)得更明顯些)。只有在求size等操作時才需要鎖定整個表。而在迭代時,ConcurrentHashMap使用了不同于傳統(tǒng)集合的快速失敗迭代器(見之前的文章《JAVA API備忘---集合》)的另一種迭代方式,我們稱為弱一致迭代器。在這種迭代方式中,當iterator被創(chuàng)建后集合再發(fā)生改變就不再是拋出 ConcurrentModificationException,取而代之的是在改變時new新的數(shù)據(jù)從而不影響原有的數(shù)據(jù),iterator完成后再將頭指針替換為新的數(shù)據(jù),這樣iterator線程可以使用原來老的數(shù)據(jù),而寫線程也可以并發(fā)的完成改變,更重要的,這保證了多個線程并發(fā)執(zhí)行的連續(xù)性和擴展性,是性能提升的關鍵。
接下來,讓我們看看ConcurrentHashMap中的幾個重要方法,心里知道了實現(xiàn)機制后,使用起來就更加有底氣。
ConcurrentHashMap中主要實體類就是三個:ConcurrentHashMap(整個Hash表),Segment(桶),HashEntry(節(jié)點),對應上面的圖可以看出之間的關系。
get 方法(請注意,這里分析的方法都是針對桶的,因為ConcurrentHashMap的最大改進就是將粒度細化到了桶上),首先判斷了當前桶的數(shù)據(jù)個數(shù)是否為0,為0自然不可能get到什么,只有返回null,這樣做避免了不必要的搜索,也用最小的代價避免出錯。然后得到頭節(jié)點(方法將在下面涉及)之后就是根據(jù)hash和key逐個判斷是否是指定的值,如果是并且值非空就說明找到了,直接返回;程序非常簡單,但有一個令人困惑的地方,這句return readValueUnderLock(e)到底是用來干什么的呢?研究它的代碼,在鎖定之后返回一個值。但這里已經(jīng)有一句V v = e.value得到了節(jié)點的值,這句return readValueUnderLock(e)是否多此一舉?事實上,這里完全是為了并發(fā)考慮的,這里當v為空時,可能是一個線程正在改變節(jié)點,而之前的 get操作都未進行鎖定,根據(jù)bernstein條件,讀后寫或?qū)懞笞x都會引起數(shù)據(jù)的不一致,所以這里要對這個e重新上鎖再讀一遍,以保證得到的是正確值,這里不得不佩服Doug Lee思維的嚴密性。整個get操作只有很少的情況會鎖定,相對于之前的Hashtable,并發(fā)是不可避免的啊!
V get(Object key, int hash) { if (count != 0) { // read-volatile HashEntry e = getFirst(hash); while (e != null) { if (e.hash == hash && key.equals(e.key)) { V v = e.value; if (v != null) return v; return readValueUnderLock(e); // recheck } e = e.next; } } | |