Java中ConcurrentHashMap學習

l_serein發表於2013-05-31

ConcurrentHashMap融合了hashtable和hashmap二者的優勢。

hashtable是做了同步的,hashmap未考慮同步。所以hashmap在單執行緒情況下效率較高。hashtable在的多執行緒情況下,同步操作能保證程式執行的正確性。

但是hashtable每次同步執行的時候都要鎖住整個結構。看下圖:

圖左側清晰的標註出來,lock每次都要鎖住整個結構。

ConcurrentHashMap正是為了解決這個問題而誕生的。

ConcurrentHashMap鎖的方式是稍微細粒度的。 ConcurrentHashMap將hash表分為16個桶(預設值),諸如get,put,remove等常用操作只鎖當前需要用到的桶。

試想,原來 只能一個執行緒進入,現在卻能同時16個寫執行緒進入(寫執行緒才需要鎖定,而讀執行緒幾乎不受限制,之後會提到),併發性的提升是顯而易見的。

更令人驚訝的是ConcurrentHashMap的讀取併發,因為在讀取的大多數時候都沒有用到鎖定,所以讀取操作幾乎是完全的併發操作,而寫操作鎖定的粒度又非常細,比起之前又更加快速(這一點在桶更多時表現得更明顯些)。只有在求size等操作時才需要鎖定整個表。

而在迭代時,ConcurrentHashMap使用了不同於傳統集合的快速失敗迭代器的另一種迭代方式,我們稱為弱一致迭代器。在這種迭代方式中,當iterator被建立後集合再發生改變就不再是丟擲 ConcurrentModificationException,取而代之的是在改變時new新的資料從而不影響原有的數 據,iterator完成後再將頭指標替換為新的資料,這樣iterator執行緒可以使用原來老的資料,而寫執行緒也可以併發的完成改變,更重要的,這保證了多個執行緒併發執行的連續性和擴充套件性,是效能提升的關鍵。

下面分析ConcurrentHashMap的原始碼。主要是分析其中的Segment。因為操作基本上都是在Segment上的。先看Segment內部資料的定義。

 

從上圖可以看出,很重要的一個是table變數。是一個HashEntry的陣列。Segment就是把資料存放在這個陣列中的。除了這個量,還有諸如loadfactor、modcount等變數。

看segment的get 函式的實現:

加上hashentry的程式碼:

可以看出,hashentry是一個連結串列型的資料結構。

在segment的get函式中,通過getFirst函式得到第一個值,然後就是通過這個值的next,一路找到想要的那個物件。如果不空,則返回。如果為空,則可能是其他執行緒正在修改節點。比如上面說的弱一致迭代器在將指標更改為新值的過程。而之前的 get操作都未進行鎖定,根據bernstein條件,讀後寫或寫後讀都會引起資料的不一致,所以這裡要對這個e重新上鎖再讀一遍,以保證得到的是正確值。readValueUnderLock中就是用了lock()進行加鎖。

put操作已開始就鎖住了整個segment。這是因為修改操作時不能併發的。

同樣,remove操作也是如此(類似put,一開始就鎖住真個segment)。

但要注意一點區別,中間那個for迴圈是做什麼用的呢?(截圖未完全,可以自己找找程式碼檢視一下)。從程式碼來看,就是將定位之後的所有entry克隆並拼回前面去,但有必要嗎?每次刪除一個元素就要將那之前的元素克隆一遍?這點其實是由entry的不變性來決定的,仔細觀察entry定義,發現除了value,其他 所有屬性都是用final來修飾的,這意味著在第一次設定了next域之後便不能再改變它,取而代之的是將它之前的節點全都克隆一次。至於entry為什麼要設定為不變性,這跟不變性的訪問不需要同步從而節省時間有關。

相關文章