ConcurrentHashMap 原始碼閱讀小結

莫那·魯道發表於2018-05-19

前言

每一次總結都意味著重新開始,同時也是為了更好的開始。ConcurrentHashMap 一直是我心中的痛。雖然不敢說完全讀懂了,但也看了幾個重要的方法,有不少我覺得比較重要的知識點。

然後呢,放一些樓主寫的關於 ConcurrentHashMap 相關原始碼分析的文章連結:

  1. ConcurrentHashMap 擴容分析拾遺
  2. 併發程式設計——ConcurrentHashMap#addCount() 分析
  3. 併發程式設計——ConcurrentHashMap#transfer() 擴容逐行分析
  4. 併發程式設計——ConcurrentHashMap#helpTransfer() 分析
  5. 併發程式設計 —— ConcurrentHashMap size 方法原理分析
  6. 併發程式設計之 ConcurrentHashMap(JDK 1.8) putVal 原始碼分析
  7. 深入理解 HashMap put 方法(JDK 8逐行剖析)
  8. 深入理解 hashcode 和 hash 演算法

putVal 方法總結

說起 ConcurrentHashMap ,當然從入口開始說。該方法要點如下:

  1. 不允許有 null key 和 null value。
  2. 只有在第一次 put 的時候才初始化 table。初始化有併發控制。通過 sizeCtl 變數判斷(小於 0)。
  3. 當 hash 對應的下標是 null 時,使用 CAS 插入元素。
  4. 當 hash 對應的下標值是 forward 時,幫助擴容,但有可能幫不了,因為每個執行緒預設 16 個桶,如果只有 16個桶,第二個執行緒是無法幫助擴容的。
  5. 如果 hash 衝突了,同步頭節點,進行連結串列操作,如果連結串列長度達到 8 ,分成紅黑樹。
  6. 呼叫 addCount 方法,對 size 加一,並判斷是否需要擴容(如果是覆蓋,就不呼叫該方法)。
  7. Cmap 的併發效能是 hashTable 的 table.length 倍。只有出現連結串列才會同步,否則使用 CAS 插入。效能極高。

size 方法總結

  1. size 方法不準確,原因是由於併發插入,baseCount 難以及時更新。計數盒子也難以及時更新。
  2. 內部通過兩個變數,一個是 baseCount,一個是 counterCells,counterCells 是併發修改 baseCount 後的備用方案。
  3. 具體更新 baseCount 和 counterCells 是在 addCount 方法中。備用方法 fullAddCount 則會死迴圈插入。
  4. CounterCell 是一個用於分配計數的填充單元,改編自 LongAdder和Striped64。內部只有一個 volatile 的 value 變數,同時這個類標記了 @sun.misc.Contended,這是一個避免偽共享的註解,用於替代之前的快取行填充。多執行緒情況下,註解讓效能提升 5 倍。

helpTransfer 方法總結

  1. 當 Cmap 嘗試插入的時候,發現該節點是 forward 型別,則會幫助其擴容。
  2. 每次加入一個執行緒都會將 sizeCtl 的低 16 位加一。同時會校驗高 16 位的標示符。
  3. 擴容最大的幫助執行緒是 65535,這是低 16 位的最大值限制的。
  4. 每個執行緒預設分配 16 個桶,如果桶的數量是 16,那麼第二個執行緒無法幫助其擴容。

transfer 方法總結

  1. 該方法會根據 CPU 核心數平均分配給每個 CPU 相同數量的桶。但如果不夠 16 個,預設就是 16 個。
  2. 擴容是按照 2 倍進行擴容。
  3. 每個執行緒在處理完自己領取的區間後,還可以繼續領取,如果有的話。這個是 transferIndex 變數遞減 16 實現的。
  4. 每次處理空桶的時候,會插入一個 forward 節點,告訴 putVal 的執行緒:“我正在擴容,快來幫忙”。但如果只有 16 個桶,只能有一個執行緒擴容。
  5. 如果有了佔位符,那就不處理,跳過這個桶。
  6. 如果有真正的實際值,那就同步頭節點,防止 putVal 那裡併發。
  7. 同步塊裡會將連結串列拆成兩份,根據 hash & length 得到是否是 0,如果是0,放在低位,反之,反之放在 length + i 的高位。這裡的設計是為了防止下次取值的時候,hash 不到正確的位置。
  8. 如果該桶的型別是紅黑樹,也會拆成 2 個,這是必須的。然後判斷拆分過的桶的大小是否小於等於 6,如果是,改成連結串列。
  9. 執行緒處理完之後,如果沒有可選區間,且任務沒有完成,就會將整個表檢查一遍,防止遺漏。

addCount 方法總結

  1. 當插入結束的時候,會對 size 進行加一。也會進行是否需要擴容的判斷。
  2. 優先使用計數盒子(如果不是空,說明併發了),如果計數盒子是空,使用 baseCount 變數。對其加 X。
  3. 如果修改 baseCount 失敗,使用計數盒子。如果此次修改失敗,在另一個方法死迴圈插入。
  4. 檢查是否需要擴容。
  5. 如果 size 大於等於 sizeCtl 閾值,且長度小於 1 << 30,可以擴容成 1 << 30,但不能擴容成 1 << 31。
  6. 如果已經在擴容,幫助其擴容,和 helpTransfer 邏輯一樣。
  7. 如果沒有在擴容,自行開啟擴容,更新 sizeCtl 變數為負數,賦值為識別符號高 16 位 + 2。

小結

ConcurrentHashMap 滿是財富,都是精華程式碼,我們這次閱讀只是管中窺豹,要知道其中包含 53 個類,6300 行程式碼,但這次確實收穫很多。有時間一定再次閱讀!!

能力不高,水平有限,有些地方確實理解不了 Doug Lea 大師的設計,如果有什麼錯誤,還請大家指出。不勝感激。

相關文章