什麼是ConcurrentHashMap?不同JDK下ConcurrentHashMap的區別?

什麼是ConcurrentHashMap?不同JDK下ConcurrentHashMap的區別?

   一、HashMap線程安全

   我們知道,在併發情況下,使用HashMap會有線程安全的問題,那麼如何避免呢?

   想要避免Hashmap的線程安全問題有很多辦法,比如改用HashTable或者Collections.synchronizedMap

   但是,這兩者有着共同的問題:性能。無論讀操作還是寫操作,他們都會給整個集合加鎖,導致同一時間的其他操作爲之阻塞。

   在併發環境下,如何能夠兼顧線程安全和運行效率呢?這時候ConcurrentHashmap就應運而生來。

    二、ConcurrentHashMap

    在 JDK1.7 的時候,ConcurrentHashMap 對整個桶數組進行了分割分段(Segment,分段鎖),每一把鎖只鎖容器其中一部分數據,多線程訪問容器裏不同數據段的數據,就不會存在鎖競爭,提高併發訪問率。

    簡單來說:ConcurrentHashMap優勢就是採用了[鎖分段技術],每一個Segment就好比自治區,讀寫操作高度自治,Segment之間互不影響。

    1.  Segment

    這裏面涉及到一個比較關鍵的概念:Segment。

    Segment本身就相當於一個HashMap對象。同HashMap一樣,Segment包含一個HashEntry數組,數組中的每一個HashEntry既是一個鍵值對,也是一個鏈表的頭節點。

    單一的Segment結構如下:

   

   像這樣的Segment對象,在ConcurrentHashMap集合中有2的N次方個,共同保存在一個名爲segments的數組當中。 

   因此整個ConcurrentHashMap的結構如下:

  

   可以說,ConcurrentHashMap是一個二級哈希表。在一個總的哈希表下面,有若干個子哈希表。

   這樣的二級結構,和數據庫的水平拆分有些相似。

   2. ConcurrentHashMap併發讀寫的幾種情形

   1)Case1: 不同Segment的併發寫入   

   說明:不同Segment的寫入是可以併發執行的。

   2)Case2: 同一Segment的一寫一讀

 

   說明:同一Segment的寫和讀是可以併發執行的。 

   3)Case3:同一Segment的併發寫入

 

  說明:Segment的寫入是需要上鎖的,因此對同一Segment的併發寫入會被阻塞。

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

  3. ConcurrentHashMap讀寫的詳細過程

  1)Get方法

  •   爲輸入的Key做Hash運算,得到hash值。
  •   通過hash值,定位到對應的Segment對象
  •   再次通過hash值,定位到Segment當中數組的具體位置。

  2)Put方法

  •   爲輸入的Key做Hash運算,得到hash值。
  •   通過hash值,定位到對應的Segment對象
  •   獲取可重入鎖
  •   再次通過hash值,定位到Segment當中數組的具體位置。
  •   插入或覆蓋HashEntry對象。

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

  4. 調用size方法時,如何解決一致性問題?

  1)分析

  這個問題Key理解爲:既然每一個Segment都各自加鎖,那麼在調用Size方法的時候,怎麼解決一致性的問題呢?

  Size方法的目的是統計ConcurrentHashMap的總元素數量, 自然需要把各個Segment內部的元素數量彙總起來。

  但是,如果在統計Segment元素數量的過程中,已統計過的Segment瞬間插入新的元素,這時候該怎麼辦呢?如下圖:

 

 

  ConcurrentHashMap的Size方法是一個嵌套循環,大體邏輯如下:

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

  官方源代碼如下:

 1 public int size() {
 2     // Try a few times to get accurate count. On failure due to
 3    // continuous async changes in table, resort to locking.
 4    final Segment<K,V>[] segments = this.segments;
 5     int size;
 6     boolean overflow; // true if size overflows 32 bits
 7     long sum;         // sum of modCounts
 8     long last = 0L;   // previous sum
 9     int retries = -1; // first iteration isn't retry
10     try {
11         for (;;) {
12             if (retries++ == RETRIES_BEFORE_LOCK) {
13                 for (int j = 0; j < segments.length; ++j)
14                     ensureSegment(j).lock(); // force creation
15             }
16             sum = 0L;
17             size = 0;
18             overflow = false;
19             for (int j = 0; j < segments.length; ++j) {
20                 Segment<K,V> seg = segmentAt(segments, j);
21                 if (seg != null) {
22                     sum += seg.modCount;
23                     int c = seg.count;
24                     if (c < 0 || (size += c) < 0)
25                         overflow = true;
26                 }
27             }
28             if (sum == last)
29                 break;
30             last = sum;
31         }
32     } finally {
33         if (retries > RETRIES_BEFORE_LOCK) {
34             for (int j = 0; j < segments.length; ++j)
35                 segmentAt(segments, j).unlock();
36         }
37     }
38     return overflow ? Integer.MAX_VALUE : size;
39 }

  2)爲什麼這樣設計呢?

  這種思想和樂觀鎖悲觀鎖的思想如出一轍。

  原因:爲了儘量不鎖住所有Segment,首先樂觀地假設Size過程中不會有修改。當嘗試一定次數,才無奈轉爲悲觀鎖,鎖住所有Segment保證強一致性。

  三、注意事項

  1. 這裏介紹的ConcurrentHashMap原理和代碼,都是基於Java1.7的。在Java8中會有些許差別。

  到了 JDK1.8 的時候,ConcurrentHashMap 已經摒棄了 Segment 的概念,而是直接用 Node 數組+鏈表+紅黑樹的數據結構來實現,併發控制使用 synchronized 和 CAS 來操作。(JDK1.6 以後 synchronized 鎖做了很多優化) 整個看起來就像是優化過且線程安全的 HashMap,雖然在 JDK1.8 中還能看到 Segment 的數據結構,但是已經簡化了屬性,只是爲了兼容舊版本。

  2.  ConcurrentHashMap在對Key求Hash值的時候,爲了實現Segment均勻分佈,進行了兩次Hash

  具體來說:爲了實現Segment的均勻分佈,採用了兩次Hash的策略。首先,它使用了傳統的Hash算法(比如將Key的hashCode取模),得到的結果稱爲哈希值。然後,它對這個哈希值再進行一次Hash操作,這個操作通常稱爲“再散列”(rehashing),目的是進一步增加哈希值的隨機性,減少哈希碰撞的概率,從而提高併發性能。這樣得到的最終哈希值用於確定Key在哪個Segment中的位置。

 

  參考資料:《程序員小灰》微信公衆號

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章