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

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

 

ConcurrentHashMap的初步使用及場景

CHM的使用算法

ConcurrentHashMap是J.U.C包裏面提供的一個線程安全而且高效的HashMap,因此ConcurrentHashMap在併發編程的場景中使用的頻率比較高,那麼這一節課咱們就從ConcurrentHashMap的使用上以及源碼層面來分析ConcurrentHashMap究竟是如何實現安全性的編程

 

api使用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版本,它作了兩個改進app

  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已經被其餘線程遷移完了。接下來分析一下它的源碼實現

一、fwd:這個類是個標識類,用於指向新表用的,其餘線程遇到這個類會主動跳過這個類,由於這個類要麼就是擴容遷移正在進行,要麼就是已經完成擴容遷移,也就是這個類要保證線程安全,再進行操做。

二、advance:這個變量是用於提示代碼是否進行推動處理,也就是當前桶處理完,處理下一個桶的標識

三、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,錄製成視頻供你們免費下載,但願能夠幫助在這個行 業發展的朋友和童鞋們,在論壇博客等地方少花些時間找資料,把有限的時間,真正花在學習上,因此我把這些資料, 分享出來。相信對於已經工做和遇到技術瓶頸或者寫博客碼友,在這份資料中必定都有你須要的內容。

相關文章
相關標籤/搜索