ConcurrentHashMap


轉自:https://www.jianshu.com/p/78989cd553b4

一、Segment

HashMap 在高並發下會出現鏈表環,從而導致程序出現死循環。高並發下避免 HashMap 出問題的方法有兩種,一是使用 HashTable,二是使用 Collections.syncronizedMap。但是這兩種方法的性能都能差。因為這兩個在執行讀寫操作時都是將整個集合加鎖,導致多個線程無法同時讀寫集合。高並發下的 HashMap 出現的問題就需要 ConcurrentHashMap 來解決了。

【介紹JDK 1.7】ConcurrentHashMap 中有一個 Segment 的概念。Segment 本身就相當於一個 HashMap 對象。同 HashMap 一樣,Segment 包含一個 HashEntry 數組,數組中的每一個 HashEntry 既是一個鍵值對,也是一個鏈表的頭節點。單一的 Segment 結構如下:

 
ConcurrentHashMap 集合中有 2 的N次方個 Segment 對象,共同保存在一個名為 segments 的數組當中。因此整個 ConcurrentHashMap 的結構如下:
 

 

 

可以說,ConcurrentHashMap 是一個二級哈希表。在一個總的哈希表下面,有若干個子哈希表。這樣的二級結構,和數據庫的水平拆分有些相似。

1️⃣ConcurrentHashMap 的優勢
采取了鎖分段技術,每一個 Segment 就好比一個自治區,讀寫操作高度自治,Segment 之間互不影響。

 

 

Case1:不同 Segment 的並發寫入【可以並發執行】
 

 

 

 

 

 

Case2:同一 Segment 的一寫一讀【可以並發執行】
 

 

 

 

 

 

Case3:同一 Segment 的並發寫入【需要上鎖】
 

 

 

由此可見,ConcurrentHashMap 當中每個 Segment 各自持有一把鎖。在保證線程安全的同時降低了鎖的粒度,讓並發操作效率更高。

2️⃣Concurrent 的讀寫過程

Get方法:

  1. 為輸入的 Key 做 Hash 運算,得到 hash 值。(為了實現Segment均勻分布,進行了兩次Hash)
  2. 通過 hash 值,定位到對應的 Segment 對象
  3. 再次通過 hash 值,定位到 Segment 當中數組的具體位置。

Put方法:

  1. 為輸入的 Key 做 Hash 運算,得到 hash 值。
  2. 通過 hash 值,定位到對應的 Segment 對象
  3. 獲取可重入鎖
  4. 再次通過 hash 值,定位到 Segment 當中數組的具體位置。
  5. 插入或覆蓋 HashEntry 對象。
  6. 釋放鎖。

從步驟可以看出,ConcurrentHashMap 在讀寫時均需要二次定位。首先定位到 Segment,之后定位到 Segment 內的具體數組下標。

二、每一個 Segment 都各自加鎖,那么在調用 size() 的時候,怎么解決一致性的問題

 

 

1️⃣調用 Size() 是統計 ConcurrentHashMap 的總元素數量,需要把各個 Segment 內部的元素數量匯總起來。但是,如果在統計 Segment 元素數量的過程中,已統計過的 Segment 瞬間插入新的元素,這時候該怎么辦呢?
 

 

 

 

 

 

 
 

 

 

 

2️⃣ConcurrentHashMap 的 Size() 是一個嵌套循環,大體邏輯如下:

  1. 遍歷所有的 Segment。
  2. 把 Segment 的元素數量累加起來。
  3. 把 Segment 的修改次數累加起來。
  4. 判斷所有 Segment 的總修改次數是否大於上一次的總修改次數。如果大於,說明統計過程中有修改,重新統計,嘗試次數+1;如果不是。說明沒有修改,統計結束。
  5. 如果嘗試次數超過閾值,則對每一個 Segment 加鎖,再重新統計。
    6.再次判斷所有 Segment 的總修改次數是否大於上一次的總修改次數。由於已經加鎖,次數一定和上次相等。
  6. 釋放鎖,統計結束。

 

 

官方源代碼如下:
 

 

 

 

 

為什么這樣設計呢?這種思想和樂觀鎖悲觀鎖的思想如出一轍。為了盡量不鎖住所有的 Segment,首先樂觀地假設 Size 過程中不會有修改。當嘗試一定次數,才無奈轉為悲觀鎖,鎖住所有 Segment 保證強一致性。

三、ConcurrentHashMap(線程安全)

  • 底層采用分段的數組+鏈表實現
  • 通過把整個 Map 分為N個 Segment,可以提供相同的線程安全,但是效率提升N倍,默認提升16倍。(讀操作不加鎖,由於 HashEntry 的 value 變量是 volatile 的,也能保證讀取到最新的值。)
  • Hashtable 的 synchronized 是針對整張 Hash 表的,即每次鎖住整張表讓線程獨占,ConcurrentHashMap 允許多個修改操作並發進行,其關鍵在於使用了鎖分離技術。
  • 有些方法需要跨段,比如 size() 和 containsValue(),它們可能需要鎖定整個表而不僅僅是某個段,這需要按順序鎖定所有段,操作完畢后,又按順序釋放所有段的鎖。
  • 擴容:段內擴容(段內元素超過該段對應 Entry 數組長度的75%觸發擴容,不會對整個 Map 進行擴容),插入前檢測是否需要擴容,避免無效擴容。
 

 

 

 

從類圖可看出在存儲結構中 ConcurrentHashMap 比 HashMap 多出了一個類 Segment,而 Segment 是一個可重入鎖。ConcurrentHashMap 是使用了鎖分段技術來保證線程安全的。

鎖分段技術:
首先將數據分成一段一段的存儲,然后給每一段數據配一把鎖,當一個線程占用鎖訪問其中一個段數據的時候,其他段的數據仍能被其他線程訪問。

ConcurrentHashMap 提供了與 Hashtable 和 SynchronizedMap 不同的鎖機制。Hashtable 中采用的鎖機制是一次鎖住整個 hash 表,從而在同一時刻只能由一個線程對其進行操作;而 ConcurrentHashMap 中則是一次鎖住一個桶。

ConcurrentHashMap 默認將 hash 表分為16個桶,諸如 get、put、remove 等常用操作只鎖住當前需要用到的桶。這樣,原來只能一個線程進入,現在卻能同時有16個寫線程執行,並發性能的提升是顯而易見的。

四、ConcurrentHashMap 1.7和1.8的區別

1️⃣整體結構

1.7:Segment + HashEntry + Unsafe
1.8: 移除 Segment,使鎖的粒度更小,Synchronized + CAS + Node + Unsafe

2️⃣put()

1.7:先定位 Segment,再定位桶,put 全程加鎖,沒有獲取鎖的線程提前找桶的位置,並最多自旋 64 次獲取鎖,超過則掛起。
1.8:由於移除了 Segment,類似 HashMap,可以直接定位到桶,拿到 first 節點后進行判斷:①為空則 CAS 插入;②為 -1 則說明在擴容,則跟着一起擴容;③ else 則加鎖 put(類似1.7)

3️⃣get()

基本類似,由於 value 聲明為 volatile,保證了修改的可見性,因此不需要加鎖。

4️⃣resize()

1.7:跟 HashMap 步驟一樣,只不過是搬到單線程中執行,避免了 HashMap 在 1.7 中擴容時死循環的問題,保證線程安全。
1.8:支持並發擴容,HashMap 擴容在1.8中由頭插改為尾插(為了避免死循環問題),ConcurrentHashmap 也是,遷移也是從尾部開始,擴容前在桶的頭部放置一個 hash 值為 -1 的節點,這樣別的線程訪問時就能判斷是否該桶已經被其他線程處理過了。

5️⃣size()

1.7:很經典的思路:計算兩次,如果不變則返回計算結果,若不一致,則鎖住所有的 Segment 求和。
1.8:用 baseCount 來存儲當前的節點個數,這就設計到 baseCount 並發環境下修改的問題。



作者:日常更新
鏈接:https://www.jianshu.com/p/78989cd553b4
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM