1. 程式人生 > >ConcurrentHashMap 源碼閱讀小結

ConcurrentHashMap 源碼閱讀小結

初始化 transfer 方法總結 最大 jdk 可能 help 之前 striped

前言

每一次總結都意味著重新開始,同時也是為了更好的開始。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 大師的設計,如果有什麽錯誤,還請大家指出。不勝感激。

ConcurrentHashMap 源碼閱讀小結