前言
每一次總結都意味著重新開始,同時也是為了更好的開始。ConcurrentHashMap 一直是我心中的痛。雖然不敢說完全讀懂了,但也看了幾個重要的方法,有不少我覺得比較重要的知識點。
然後呢,放一些樓主寫的關於 ConcurrentHashMap 相關原始碼分析的文章連結:
- ConcurrentHashMap 擴容分析拾遺
- 併發程式設計——ConcurrentHashMap#addCount() 分析
- 併發程式設計——ConcurrentHashMap#transfer() 擴容逐行分析
- 併發程式設計——ConcurrentHashMap#helpTransfer() 分析
- 併發程式設計 —— ConcurrentHashMap size 方法原理分析
- 併發程式設計之 ConcurrentHashMap(JDK 1.8) putVal 原始碼分析
- 深入理解 HashMap put 方法(JDK 8逐行剖析)
- 深入理解 hashcode 和 hash 演算法
putVal 方法總結
說起 ConcurrentHashMap ,當然從入口開始說。該方法要點如下:
- 不允許有 null key 和 null value。
- 只有在第一次 put 的時候才初始化 table。初始化有併發控制。通過 sizeCtl 變數判斷(小於 0)。
- 當 hash 對應的下標是 null 時,使用 CAS 插入元素。
- 當 hash 對應的下標值是 forward 時,幫助擴容,但有可能幫不了,因為每個執行緒預設 16 個桶,如果只有 16個桶,第二個執行緒是無法幫助擴容的。
- 如果 hash 衝突了,同步頭節點,進行連結串列操作,如果連結串列長度達到 8 ,分成紅黑樹。
- 呼叫 addCount 方法,對 size 加一,並判斷是否需要擴容(如果是覆蓋,就不呼叫該方法)。
- Cmap 的併發效能是 hashTable 的 table.length 倍。只有出現連結串列才會同步,否則使用 CAS 插入。效能極高。
size 方法總結
- size 方法不準確,原因是由於併發插入,baseCount 難以及時更新。計數盒子也難以及時更新。
- 內部通過兩個變數,一個是 baseCount,一個是 counterCells,counterCells 是併發修改 baseCount 後的備用方案。
- 具體更新 baseCount 和 counterCells 是在 addCount 方法中。備用方法 fullAddCount 則會死迴圈插入。
- CounterCell 是一個用於分配計數的填充單元,改編自 LongAdder和Striped64。內部只有一個 volatile 的 value 變數,同時這個類標記了
@sun.misc.Contended
,這是一個避免偽共享的註解,用於替代之前的快取行填充。多執行緒情況下,註解讓效能提升 5 倍。
helpTransfer 方法總結
- 當 Cmap 嘗試插入的時候,發現該節點是 forward 型別,則會幫助其擴容。
- 每次加入一個執行緒都會將 sizeCtl 的低 16 位加一。同時會校驗高 16 位的標示符。
- 擴容最大的幫助執行緒是 65535,這是低 16 位的最大值限制的。
- 每個執行緒預設分配 16 個桶,如果桶的數量是 16,那麼第二個執行緒無法幫助其擴容。
transfer 方法總結
- 該方法會根據 CPU 核心數平均分配給每個 CPU 相同數量的桶。但如果不夠 16 個,預設就是 16 個。
- 擴容是按照 2 倍進行擴容。
- 每個執行緒在處理完自己領取的區間後,還可以繼續領取,如果有的話。這個是 transferIndex 變數遞減 16 實現的。
- 每次處理空桶的時候,會插入一個 forward 節點,告訴 putVal 的執行緒:“我正在擴容,快來幫忙”。但如果只有 16 個桶,只能有一個執行緒擴容。
- 如果有了佔位符,那就不處理,跳過這個桶。
- 如果有真正的實際值,那就同步頭節點,防止 putVal 那裡併發。
- 同步塊裡會將連結串列拆成兩份,根據 hash & length 得到是否是 0,如果是0,放在低位,反之,反之放在 length + i 的高位。這裡的設計是為了防止下次取值的時候,hash 不到正確的位置。
- 如果該桶的型別是紅黑樹,也會拆成 2 個,這是必須的。然後判斷拆分過的桶的大小是否小於等於 6,如果是,改成連結串列。
- 執行緒處理完之後,如果沒有可選區間,且任務沒有完成,就會將整個表檢查一遍,防止遺漏。
addCount 方法總結
- 當插入結束的時候,會對 size 進行加一。也會進行是否需要擴容的判斷。
- 優先使用計數盒子(如果不是空,說明併發了),如果計數盒子是空,使用 baseCount 變數。對其加 X。
- 如果修改 baseCount 失敗,使用計數盒子。如果此次修改失敗,在另一個方法死迴圈插入。
- 檢查是否需要擴容。
- 如果 size 大於等於 sizeCtl 閾值,且長度小於 1 << 30,可以擴容成 1 << 30,但不能擴容成 1 << 31。
- 如果已經在擴容,幫助其擴容,和 helpTransfer 邏輯一樣。
- 如果沒有在擴容,自行開啟擴容,更新 sizeCtl 變數為負數,賦值為識別符號高 16 位 + 2。
小結
ConcurrentHashMap 滿是財富,都是精華程式碼,我們這次閱讀只是管中窺豹,要知道其中包含 53 個類,6300 行程式碼,但這次確實收穫很多。有時間一定再次閱讀!!
能力不高,水平有限,有些地方確實理解不了 Doug Lea 大師的設計,如果有什麼錯誤,還請大家指出。不勝感激。