天天看點

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(節點),對應上面的圖可以看出之間的關系。

   ConcurrentHashMap為了提高本身的并發能力,在内部采用了一個叫做Segment的結構,一個Segment其實就是一個類Hash Table的結構,Segment内部維護了一個連結清單數組,我們用下面這一幅圖來看下ConcurrentHashMap的内部結構:

ConcurrentHashMap原理分析

  從上面的結構我們可以了解到,ConcurrentHashMap定位一個元素的過程需要進行兩次Hash操作,第一次Hash定位到Segment,第二次Hash定位到元素所在的連結清單的頭部,是以,這一種結構的帶來的副作用是Hash的過程要比普通的HashMap要長,但是帶來的好處是寫操作的時候可以隻對元素所在的Segment進行加鎖即可,不會影響到其他的Segment,這樣,在最理想的情況下,ConcurrentHashMap可以最高同時支援Segment數量大小的寫操作(剛好這些寫操作都非常平均地分布在所有的Segment上),是以,通過這一種結構,ConcurrentHashMap的并發能力可以大大的提高。