并發永遠是高性能的話題,而并發容器又是java中重要的并發工具,所以今天我們來分析一下Concurrent包中ConcurrentHashMap(以下簡稱Chashmap)。普通容器在某些并發情況下的表現很差,假設這容器的體積很大,容器獲得鎖后進行了非常耗時的遍歷操作,那么鎖就會被占用很長世間。事實上只有很小一部分元素被線程占用,其余的元素完全可以被讀寫。而(以下簡稱Chashmap就是實現了這一特性,使用該容器一部分區域的時候其他線程可以同時讀寫容器中的剩余區域,所以Chashmap的效率很高。
Chashmap的關鍵實現是一種分段鎖機制,簡單來講就是將hash表分成一段段子表,分別加鎖。這樣線程需要占用哪一段就獲取哪一段的鎖,剩余的段不受影響。本質上Chashmap和hashMap沒什么區別,元素都是Entry,一個節點加鏈表。發生沖突的時候采用鏈表法存儲元素,所以源碼里會有大量的通過key的hash值找到槽后進行遍歷查詢的操作。下面是子表的聲明:
自定義了一個segment類,繼承了可重入鎖。可以看一下整體的結構:
其中比較關鍵的應該是put,rehash,scanandlock,remove,replace等方法。
我們先來看segment的put方法:
對map中的某個segment做put操作,加入我們來實現一般會考慮到幾點:
需要加鎖,因為要考慮到key相同的情況,如果不加鎖,兩個線程同時更新同一個key的value,會出錯。
1)需要考慮鎖已經被占的情況。
2)需要更新segment的元素個數。
3)需要考慮segment擴容的問題。
帶著這些考慮,我們再來看源碼可能就會更有針對性一些。
第一步果然是加鎖,這里直接用了trylock,因為segment繼承了可重入鎖,其實這是個很垃圾的設計,按理來說應該優先使用組合而不是繼承。我們可以看到,嘗試獲取鎖失敗后,調用了scanAndLockForPut方法,這個方法會不斷嘗試獲取鎖,同時去匹配entry的key值,直到成功,重新回到put方法。Put方法里考慮到了擴容的問題:
假如元素個數大于閾值,會調用rehash方法。Rehash首先進行了數組的擴容:
同時對老的元素進行遷移的時候進行了rehash操作: idx = e.hash&sizeMask
最后put方法會更新元素總數:
下面我們來看一下remove方法:
1)同樣的我們首先來自己思考一下哪些點是需要考慮到的:
2)需要枷鎖,同時考慮鎖被搶占,同插入
3)更新元素個數
首先是嘗試獲取鎖,如果鎖被占了,那么等待獲取。
查找的時候先獲取Entry,然后遍歷鏈表:
找到元素后進行普通的鏈表移除元素操作,并更新元素總數。
這里有點奇怪的地方,modeCount為什么加一了?這點后面解答。
Replace和remove操作幾乎一致,這里不再贅述。
看完segment的主要方法我們再來仔細看一下Chashmap的方法,首先是put方法:
首先要獲取對應的segment,然后委托給具體的segment。這里有個細節,對于value為null的情況拋出了異常。
對于remove方法同樣也是委托執行:
'
從上述代碼可以看出put和remove方法都是委托給底層的segment執行,所以不復雜,倒是size這種需要統計的方法因為要考慮到并發更改元素的情況會比較復雜。Size方法需要考慮幾點
統計過程中各個segment的元素個數可能會被動態改變的情況怎么檢測,如何處理?
不斷重試統計的標準是什么?即什么時候停止重試?
假設重試次數過多應該采取什么策略?
我們看到size的代碼可以看出上述3個疑問的解答:
對于其他類似的讀取操作,比如contains操作,也會有在讀取期間發生元素被添加/移除的情況。采取的策略也是一致的,讀取兩次并對比,如果不一致就加鎖。
最后我們來看一下get方法:
沒什么特別的地方,就是一個定位查詢。
版權聲明:本文為博主原創文章,未經博主允許不得轉載。
新聞熱點
疑難解答