ConcurrentHashMap工作原理
ConcurrentHashMap採用了分段鎖的設計,只有在同一個分段內才存在競態關係,不同的分段鎖之間沒有鎖競爭。
相比於對整個Map加鎖的設計,分段鎖大大的提高了高併發環境下的處理能力。
ConcurrentHashMap中的分段鎖稱為Segment
同時又是一個ReentrantLock(Segment繼承了ReentrantLock)。
併發度可以理解為程式執行時能夠同時更新ConccurentHashMap且不產生鎖競爭的最大執行緒數,
實際上就是ConcurrentHashMap中的分段鎖個數,
即Segment[]的陣列長度。ConcurrentHashMap預設的併發度為16,
但使用者也可以在建構函式中設定併發度。當用戶設定併發度時,ConcurrentHashMap會使用大於等於該值的最小2冪指數作為實際併發度(假如使用者設定併發度為17,實際併發度則為32)。
如果併發度設定的過小,會帶來嚴重的鎖競爭問題;如果併發度設定的過大,原本位於同一個Segment內的訪問會擴散到不同的Segment中,CPU cache命中率會下降,從而引起程式效能下降。
JDK1.8
它摒棄了Segment(鎖段)的概念,而是啟用了一種全新的方式實現,利用CAS演算法。
它沿用了與它同時期的HashMap版本的思想,底層依然由“陣列”+連結串列+紅黑樹的方式思想(JDK7與JDK8中HashMap的實現),但是為了做到併發,又增加了很多輔助的類,
例如TreeBin,Traverser等物件內部類。
Node是最核心的內部類,它包裝了key-value鍵值對,所有插入ConcurrentHashMap的資料都包裝在這裡面。
它與HashMap中的定義很相似,但是但是有一些差別它對value和next屬性設定了volatile同步鎖(與JDK7的Segment相同),
它不允許呼叫setValue方法直接改變Node的value域,它增加了find方法輔助map.get()方法。
TreeNode(樹節點類),另外一個核心的資料結構。當連結串列長度過長的時候,會轉換為TreeNode。
但是與HashMap不相同的是,它並不是直接轉換為紅黑樹,而是把這些結點包裝成TreeNode放在TreeBin物件中,由TreeBin完成對紅黑樹的包裝。
而且TreeNode在ConcurrentHashMap整合自Node類,而並非HashMap中的整合自LinkedHashMap.Entry<K,V>類,也就是說TreeNode帶有next指標,這樣做的目的是方便基於TreeBin的訪問。
TreeBin這個類並不負責包裝使用者的key、value資訊,而是包裝的很多TreeNode節點。
它代替了TreeNode的根節點,也就是說在實際的ConcurrentHashMap“陣列”中,存放的是TreeBin物件,而不是TreeNode物件,這是與HashMap的區別。
另外這個類還帶有了讀寫鎖。
CAS演算法
這個方法是利用一個CAS演算法實現無鎖化的修改值的操作,他可以大大降低鎖代理的效能消耗。
這個演算法的基本思想就是不斷地去比較當前記憶體中的變數值與你指定的一個變數值是否相等,如果相等,則接受你指定的修改的值,否則拒絕你的操作。
整個擴容操作分為兩個部分
第一部分是構建一個nextTable,它的容量是原來的兩倍,這個操作是單執行緒完成的。這個單執行緒的保證是通過RESIZE_STAMP_SHIFT這個常量經過一次運算來保證的,這個地方在後面會有提到;
第二個部分就是將原來table中的元素複製到nextTable中,這裡允許多執行緒進行操作。
put()方法
有一個最重要的不同點就是ConcurrentHashMap不允許key或value為null值。另外由於涉及到多執行緒,put方法就要複雜一點。在多執行緒中可能有以下兩個情況
如果這個位置是空的,那麼直接放入,而且不需要加鎖操作
至於為什麼JDK8中使用synchronized而不是ReentrantLock,我猜是因為JDK8中對synchronized有了足夠的優化吧。