1. 程式人生 > >阿里十年架構師,教你深度分析ConcurrentHashMap原理分析

阿里十年架構師,教你深度分析ConcurrentHashMap原理分析

阿里十年架構師,教你深度分析ConcurrentHashMap原理分析

 

ConcurrentHashMap的初步使用及場景

CHM的使用

ConcurrentHashMap是J.U.C包裡面提供的一個執行緒安全並且高效的HashMap,所以ConcurrentHashMap在併發程式設計的場景中使用的頻率比較高,那麼這一節課我們就從ConcurrentHashMap的使用上以及原始碼層面來分析ConcurrentHashMap到底是如何實現安全性的

 

api使用

ConcurrentHashMap是Map的派生類,所以api基本和Hashmap是類似,主要就是put、get這些方法,接下來基於ConcurrentHashMap的put和get這兩個方法作為切入點來分析ConcurrentHashMap的原始碼實現

ConcurrentHashMap的原始碼分析

先要做一個說明,這節課分析的ConcurrentHashMap是基於Jdk1.8的版本。

JDK1.7和Jdk1.8版本的變化

ConcurrentHashMap和HashMap的實現原理是差不多的,但是因為ConcurrentHashMap需要支援併發操作,所以在實現上要比hashmap稍微複雜一些。

在JDK1.7的實現上,ConrruentHashMap由一個個Segment組成,簡單來說,ConcurrentHashMap是一個Segment陣列,它通過繼承ReentrantLock來進行加鎖,通過每次鎖住一個segment來保證每個segment內的操作的執行緒安全性從而實現全域性執行緒安全。整個結構圖如下

 

阿里十年架構師,教你深度分析ConcurrentHashMap原理分析

 

 

當每個操作分佈在不同的segment上的時候,預設情況下,理論上可以同時支援16個執行緒的併發寫入。

相比於1.7版本,它做了兩個改進

  1. 取消了segment分段設計,直接使用Node陣列來儲存資料,並且採用Node陣列元素作為鎖來實現每一行資料進行加鎖來進一步減少併發衝突的概率
  2. 將原本陣列+單向連結串列的資料結構變更為了陣列+單向連結串列+紅黑樹的結構。為什麼要引入紅黑樹呢?在正常情況下,key hash之後如果能夠很均勻的分散在陣列中,那麼table陣列中的每個佇列的長度主要為0或者1.但是實際情況下,還是會存在一些佇列長度過長的情況。如果還採用單向列表方式,那麼查詢某個節點的時間複雜度就變為O(n); 因此對於佇列長度超過8的列表,JDK1.8採用了紅黑樹的結構,那麼查詢的時間複雜度就會降低到O(logN),可以提升查詢的效能;

 

阿里十年架構師,教你深度分析ConcurrentHashMap原理分析

 

這個結構和JDK1.8版本中的Hashmap的實現結構基本一致,但是為了保證執行緒安全性,ConcurrentHashMap的實現會稍微複雜一下。接下來我們從原始碼層面來了解一下它的原理.

我們基於put和get方法來分析它的實現即可

put方法第一階段

阿里十年架構師,教你深度分析ConcurrentHashMap原理分析

 

假如在上面這段程式碼中存在兩個執行緒,在不加鎖的情況下:執行緒A成功執行casTabAt操作後,隨後的執行緒B可以通過tabAt方法立刻看到table[i]的改變。原因如下:執行緒A的casTabAt操作,具有volatile讀寫相同的記憶體語義,根據volatile的happens-before規則:執行緒A的casTabAt操作,一定對執行緒B的tabAt操作可見

initTable

陣列初始化方法,這個方法比較簡單,就是初始化一個合適大小的陣列

sizeCtl這個要單獨說一下,如果沒搞懂這個屬性的意義,可能會被搞暈

這個標誌是在Node陣列初始化或者擴容的時候的一個控制位標識,負數代表正在進行初始化或者擴容操作

-1 代表正在初始化

-N 代表有N-1有二個執行緒正在進行擴容操作,這裡不是簡單的理解成n個執行緒,sizeCtl就是-N,這塊後續在講擴容的時候會說明

0標識Node陣列還沒有被初始化,正數代表初始化或者下一次擴容的大小

阿里十年架構師,教你深度分析ConcurrentHashMap原理分析

 

tabAt

該方法獲取物件中offset偏移地址對應的物件field的值。實際上這段程式碼的含義等價於tab[i], 但是為什麼不直接使用tab[i]來計算呢?

getObjectVolatile,一旦看到volatile關鍵字,就表示可見性。因為對volatile寫操作happen-before於volatile讀操作,因此其他執行緒對table的修改均對get讀取可見;

雖然table陣列本身是增加了volatile屬性,但是“volatile的陣列只針對陣列的引用具有volatile的語義,而不是它的元素”。 所以如果有其他執行緒對這個陣列的元素進行寫操作,那麼當前執行緒來讀的時候不一定能讀到最新的值。

出於效能考慮,Doug Lea直接通過Unsafe類來對table進行操作。

阿里十年架構師,教你深度分析ConcurrentHashMap原理分析

 

圖解分析

 

阿里十年架構師,教你深度分析ConcurrentHashMap原理分析

 

 

put方法第二階段

在putVal方法執行完成以後,會通過addCount來增加ConcurrentHashMap中的元素個數,並且還會可能觸發擴容操作。這裡會有兩個非常經典的設計

  1. 高併發下的擴容
  2. 如何保證addCount的資料安全性以及效能

阿里十年架構師,教你深度分析ConcurrentHashMap原理分析

 

addCount

在putVal最後呼叫addCount的時候,傳遞了兩個引數,分別是1和binCount(連結串列長度),看看addCount方法裡面做了什麼操作

x表示這次需要在表中增加的元素個數,check引數表示是否需要進行擴容檢查,大於等於0都需要進行檢查

阿里十年架構師,教你深度分析ConcurrentHashMap原理分析

 

CounterCells解釋

ConcurrentHashMap是採用CounterCell陣列來記錄元素個數的,像一般的集合記錄集合大小,直接定義一個size的成員變數即可,當出現改變的時候只要更新這個變數就行。為什麼ConcurrentHashMap要用這種形式來處理呢?

問題還是處在併發上,ConcurrentHashMap是併發集合,如果用一個成員變數來統計元素個數的話,為了保證併發情況下共享變數的的難全興,勢必會需要通過加鎖或者自旋來實現,如果競爭比較激烈的情況下,size的設定上會出現比較大的衝突反而影響了效能,所以在ConcurrentHashMap採用了分片的方法來記錄大小,具體什麼意思,我們來分析下

阿里十年架構師,教你深度分析ConcurrentHashMap原理分析

 

fullAddCount原始碼分析

fullAddCount主要是用來初始化CounterCell,來記錄元素個數,裡面包含擴容,初始化等操作

阿里十年架構師,教你深度分析ConcurrentHashMap原理分析

 

阿里十年架構師,教你深度分析ConcurrentHashMap原理分析

 

初始化CounterCells陣列

阿里十年架構師,教你深度分析ConcurrentHashMap原理分析

 

CounterCells初始化圖解

初始化長度為2的陣列,然後隨機得到指定的一個數組下標,將需要新增的值加入到對應下標位置處

 

阿里十年架構師,教你深度分析ConcurrentHashMap原理分析

 

 

transfer擴容階段

判斷是否需要擴容,也就是當更新後的鍵值對總數baseCount >= 閾值sizeCtl時,進行rehash,這裡面會有兩個邏輯。

  1. 如果當前正在處於擴容階段,則當前執行緒會加入並且協助擴容
  2. 如果當前沒有在擴容,則直接觸發擴容操作

阿里十年架構師,教你深度分析ConcurrentHashMap原理分析

 

resizeStamp

這塊邏輯要理解起來,也有一點複雜。

resizeStamp用來生成一個和擴容有關的擴容戳,具體有什麼作用呢?我們基於它的實現來做一個分析

阿里十年架構師,教你深度分析ConcurrentHashMap原理分析

 

transfer

擴容是ConcurrentHashMap的精華之一,擴容操作的核心在於資料的轉移,在單執行緒環境下資料的轉移很簡單,無非就是把舊陣列中的資料遷移到新的陣列。但是這在多執行緒環境下,在擴容的時候其他執行緒也可能正在新增元素,這時又觸發了擴容怎麼辦?可能大家想到的第一個解決方案是加互斥鎖,把轉移過程鎖住,雖然是可行的解決方案,但是會帶來較大的效能開銷。因為互斥鎖會導致所有訪問臨界區的執行緒陷入到阻塞狀態,持有鎖的執行緒耗時越長,其他競爭執行緒就會一直被阻塞,導致吞吐量較低。而且還可能導致死鎖。

而ConcurrentHashMap並沒有直接加鎖,而是採用CAS實現無鎖的併發同步策略,最精華的部分是它可以利用多執行緒來進行協同擴容

簡單來說,它把Node陣列當作多個執行緒之間共享的任務佇列,然後通過維護一個指標來劃分每個執行緒鎖負責的區間,每個執行緒通過區間逆向遍歷來實現擴容,一個已經遷移完的bucket會被替換為一個ForwardingNode節點,標記當前bucket已經被其他執行緒遷移完了。接下來分析一下它的原始碼實現

1、fwd:這個類是個標識類,用於指向新表用的,其他執行緒遇到這個類會主動跳過這個類,因為這個類要麼就是擴容遷移正在進行,要麼就是已經完成擴容遷移,也就是這個類要保證執行緒安全,再進行操作。

2、advance:這個變數是用於提示程式碼是否進行推進處理,也就是當前桶處理完,處理下一個桶的標識

3、finishing:這個變數用於提示擴容是否結束用的

阿里十年架構師,教你深度分析ConcurrentHashMap原理分析

 

阿里十年架構師,教你深度分析ConcurrentHashMap原理分析

 

擴容過程圖解

ConcurrentHashMap支援併發擴容,實現方式是,把Node陣列進行拆分,讓每個執行緒處理自己的區域,假設table陣列總長度是64,預設情況下,那麼每個執行緒可以分到16個bucket。

然後每個執行緒處理的範圍,按照倒序來做遷移

通過for自迴圈處理每個槽位中的連結串列元素,預設advace為真,通過CAS設定transferIndex屬性值,並初始化i和bound值,i指當前處理的槽位序號,bound指需要處理的槽位邊界,先處理槽位31的節點; (bound,i) =(16,31) 從31的位置往前推動。

 

阿里十年架構師,教你深度分析ConcurrentHashMap原理分析

 

 

假設這個時候ThreadA在進行transfer,那麼邏輯圖表示如下

 

阿里十年架構師,教你深度分析ConcurrentHashMap原理分析

 

 

在當前假設條件下,槽位15中沒有節點,則通過CAS插入在第二步中初始化的ForwardingNode節點,用於告訴其它執行緒該槽位已經處理過了;

 

阿里十年架構師,教你深度分析ConcurrentHashMap原理分析

 

 

sizeCtl擴容退出機制

在擴容操作transfer的第2414行,程式碼如下

阿里十年架構師,教你深度分析ConcurrentHashMap原理分析

 

高低位原理分析

ConcurrentHashMap在做連結串列遷移時,會用高低位來實現,這裡有兩個問題要分析一下

如何實現高低位連結串列的區分 假如我們有這樣一個佇列

 

阿里十年架構師,教你深度分析ConcurrentHashMap原理分析

 

 

第14個槽位插入新節點之後,連結串列元素個數已經達到了8,且陣列長度為16,優先通過擴容來緩解連結串列過長的問題,擴容這塊的圖解稍後再分析,先分析高低位擴容的原理

假如當前執行緒正在處理槽位為14的節點,它是一個連結串列結構,在程式碼中,首先定義兩個變數節點ln和hn,實際就是lowNode和HighNode,分別儲存hash值的第x位為0和不等於0的節點

通過fn&n可以把這個連結串列中的元素分為兩類,A類是hash值的第X位為0,B類是hash值的第x位為不等於0(至於為什麼要這麼區分,稍後分析),並且通過lastRun記錄最後要處理的節點。最終要達到的目的是,A類的連結串列保持位置不動,B類的連結串列為14+16(擴容增加的長度)=30

我們把14槽位的連結串列單獨伶出來,我們用藍色表示 fn&n=0的節點,假如連結串列的分類是這樣

 

阿里十年架構師,教你深度分析ConcurrentHashMap原理分析

 

 

阿里十年架構師,教你深度分析ConcurrentHashMap原理分析

 

阿里十年架構師,教你深度分析ConcurrentHashMap原理分析

 

接著,通過CAS操作,把hn鏈放在i+n也就是14+16的位置,ln鏈保持原來的位置不動。並且設定當前節點為fwd,表示已經被當前執行緒遷移完了

阿里十年架構師,教你深度分析ConcurrentHashMap原理分析

 

遷移完成以後的資料分佈如下

 

阿里十年架構師,教你深度分析ConcurrentHashMap原理分析

 

 

為什麼要做高低位的劃分

要想了解這麼設計的目的,我們需要從ConcurrentHashMap的根據下標獲取物件的演算法來看,在putVal方法中1018行

阿里十年架構師,教你深度分析ConcurrentHashMap原理分析

 

阿里十年架構師,教你深度分析ConcurrentHashMap原理分析

 

擴容結束以後的退出機制

如果執行緒擴容結束,那麼需要退出,就會執行transfer方法的如下程式碼

阿里十年架構師,教你深度分析ConcurrentHashMap原理分析

 

put方法第三階段

如果對應的節點存在,判斷這個節點的hash是不是等於MOVED(-1),說明當前節點是ForwardingNode節點,

意味著有其他執行緒正在進行擴容,那麼當前現在直接幫助它進行擴容,因此呼叫helpTransfer方法

阿里十年架構師,教你深度分析ConcurrentHashMap原理分析

 

put方法第四階段

這個方法的主要作用是,如果被新增的節點的位置已經存在節點的時候,需要以連結串列的方式加入到節點中

如果當前節點已經是一顆紅黑樹,那麼就會按照紅黑樹的規則將當前節點加入到紅黑樹中

阿里十年架構師,教你深度分析ConcurrentHashMap原理分析

 

put方法第五個階段

判斷連結串列的長度是否已經達到臨界值8. 如果達到了臨界值,這個時候會根據當前陣列的長度來決定是擴容還是將連結串列轉化為紅黑樹。也就是說如果當前陣列的長度小於64,就會先擴容。否則,會把當前連結串列轉化為紅黑樹

阿里十年架構師,教你深度分析ConcurrentHashMap原理分析

 

treeifyBin

在putVal的最後部分,有一個判斷,如果連結串列長度大於8,那麼就會觸發擴容或者紅黑樹的轉化操作。

阿里十年架構師,教你深度分析ConcurrentHashMap原理分析

 

tryPresize

tryPresize裡面部分程式碼和addCount的部分程式碼類似,看起來會稍微簡單一些

阿里十年架構師,教你深度分析ConcurrentHashMap原理分析

文章中涉及到的技術點我都分享在Java架構社群 142019080 裡或者+V:JaneS0307,錄製成視訊供大家免費下載,希望可以幫助在這個行
業發展的朋友和童鞋們,在論壇部落格等地方少花些時間找資料,把有限的時間,真正花在學習上,所以我把這些資料,
分享出來。相信對於已經工作和遇到技術瓶頸或者寫部落格碼友,在這份資料中一定都有