ConcurrentHashMap的實現原理

wangyunpeng0319發表於2017-06-03

ConcurrentHashMap是Java1.5中引用的一個執行緒安全的支援高併發的HashMap集合類。這篇文章總結了ConcurrentHashMap的內部實現原理,是對於自己理解後的一些整理。


1.HashTable與ConcurrentHashMap的對比

HashTable本身是執行緒安全的,寫過Java程式的都知道通過加Synchronized關鍵字實現執行緒安全,這樣對整張表加鎖實現同步的一個缺陷就在於使程式的效率變得很低。這就是為什麼Java中會在1.5後引入ConcurrentHashMap的原因。

內部結構的對比

從圖中可以看出,HashTable的鎖加在整個Hash表上,而ConcurrentHashMap將鎖加在segment上(每個段上),這樣我們在對segment1操作的時候,同時也可以對segment2中的資料操作,這樣效率就會高很多。

2.ConcurrentHashMap的內部結構

這裡寫圖片描述

ConcurrentHashMap主要有三大結構:整個Hash表,segment(段),HashEntry(節點)。每個segment就相當於一個HashTable。

(1)HashEntry類

每個HashEntry代表Hash表中的一個節點,在其定義的結構中可以看到,除了value值沒有定義final,其餘的都定義為final型別,我們知道Java中關鍵詞final修飾的域成為最終域。用關鍵詞final修飾的變數一旦賦值,就不能改變,也稱為修飾的標識為常量。這就意味著我們刪除或者增加一個節點的時候,就必須從頭開始重新建立Hash鏈,因為next引用值需要改變。

static final class HashEntry<K,V> { 
        final K key;                 // 宣告 key 為 final 型
        final int hash;              // 宣告 hash 值為 final 型 
        volatile V value;           // 宣告 value 為 volatile 型
        final HashEntry<K,V> next;  // 宣告 next 為 final 型 

        HashEntry(K key, int hash, HashEntry<K,V> next, V value)  { 
            this.key = key; 
            this.hash = hash; 
            this.next = next; 
            this.value = value; 
        } 
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

由於這樣的特性,所以插入Hash鏈中的資料都是從頭開始插入的。例如將A,B,C插入空桶中,插入後的結構為: 
這裡寫圖片描述

(2)segment類

Segment 類繼承於 ReentrantLock 類,從而使得 Segment 物件能充當鎖的角色。每個 Segment 物件用來守護其(成員物件 table 中)包含的若干個桶。

table 是一個由 HashEntry 物件組成的陣列。table 陣列的每一個陣列成員就是雜湊對映表的一個桶。

count 變數是一個計數器,它表示每個 Segment 物件管理的 table 陣列(若干個 HashEntry 組成的連結串列)包含的 HashEntry 物件的個數。每一個 Segment 物件都有一個 count 物件來表示本 Segment 中包含的 HashEntry 物件的總數。注意,之所以在每個 Segment 物件中包含一個計數器,而不是在 ConcurrentHashMap 中使用全域性的計數器,是為了避免出現“熱點域”而影響 ConcurrentHashMap 的併發性。

static final class Segment<K,V> extends ReentrantLock implements Serializable {  
 private static final long serialVersionUID = 2249069246763182397L;  
         /** 
          * 在本 segment 範圍內,包含的 HashEntry 元素的個數
          * 該變數被宣告為 volatile 型,保證每次讀取到最新的資料
          */  
         transient volatile int count;  

         /** 
          *table 被更新的次數
          */  
         transient int modCount;  

         /** 
          * 當 table 中包含的 HashEntry 元素的個數超過本變數值時,觸發 table 的再雜湊
          */  
         transient int threshold;  

         /** 
          * table 是由 HashEntry 物件組成的陣列
          * 如果雜湊時發生碰撞,碰撞的 HashEntry 物件就以連結串列的形式連結成一個連結串列
          * table 陣列的陣列成員代表雜湊對映表的一個桶
          * 每個 table 守護整個 ConcurrentHashMap 包含桶總數的一部分
          * 如果併發級別為 16,table 則守護 ConcurrentHashMap 包含的桶總數的 1/16 
          */  
         transient volatile HashEntry<K,V>[] table;  

         /** 
          * 裝載因子
          */  
         final float loadFactor;  
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
ConcurrentHashMap 類

預設的情況下,每個ConcurrentHashMap 類會建立16個併發的segment,每個segment裡面包含多個Hash表,每個Hash鏈都是有HashEntry節點組成的。

 public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>  
         implements ConcurrentMap<K, V>, Serializable {  
     /** 
      * segments 的掩碼值
      * key 的雜湊碼的高位用來選擇具體的 segment  
      */  
     final int segmentMask;  

     /** 
      * 偏移量
      */  
     final int segmentShift;  

     /** 
      * 由 Segment 物件組成的陣列,每個都是一個特別的Hash Table
      */  
     final Segment<K,V>[] segments;  
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

3.用分離鎖實現多個執行緒間的併發寫操作

(1)Put方法的實現
public V put(K key, V value) {  
        if (value == null)  //ConcurrentHashMap 中不允許用 null 作為對映值
            throw new NullPointerException();  
        int hash = hash(key.hashCode()); //計算鍵對應的雜湊碼 

        //根據雜湊碼找到對應的 Segment 
        return segmentFor(hash).put(key, hash, value, false);  
    }  

 V put(K key, int hash, V value, boolean onlyIfAbsent) {  
            lock();    //當前的segment加鎖
            try {  
                int c = count;  
                if (c++ > threshold) //如果超過再雜湊的閾值 
                    rehash(); //執行再雜湊,table 陣列的長度將擴充一倍  
                HashEntry<K,V>[] tab = table;  

                //把雜湊碼值與 table 陣列的長度減 1 的值相“與”
                //得到該雜湊碼對應的 table 陣列的下標值
                int index = hash & (tab.length - 1);  

                //找到雜湊碼對應的具體的那個桶
                HashEntry<K,V> first = tab[index];  
                HashEntry<K,V> e = first;  
                while (e != null && (e.hash != hash || !key.equals(e.key)))  
                    e = e.next;  

                V oldValue;  
                if (e != null) { //如果鍵/值對以經存在 
                    oldValue = e.value;  
                    if (!onlyIfAbsent)  
                        e.value = value; // 設定 value 值 
                }  
                else {  //鍵/值對不存在  
                    oldValue = null;  
                    ++modCount; //新增新節點到連結串列中,modCont 要加 1  

                    // 建立新節點,並新增到連結串列的頭部 
                    tab[index] = new HashEntry<K,V>(key, hash, first, value);  
                    count = c; //寫 count 變數 
                }  
                return oldValue;  
            } finally {  
                unlock(); //解鎖 
            }  
        }  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

整個程式碼通過註釋很好理解了,稍微要注意的是這裡的加鎖是針對具體的segment,而不是對整個ConcurrentHashMap。Put方法從原始碼上可以看出是從連結串列的頭部插入新的資料的。

(2)Get方法的實現
V get(Object key, int hash) { 
            if(count != 0) {       // 首先讀 count 變數
                HashEntry<K,V> e = getFirst(hash); 
                while(e != null) { 
                    if(e.hash == hash && key.equals(e.key)) { 
                        V v = e.value; 
                        if(v != null)            
                            return v; 
                        // 如果讀到 value 域為 null,說明發生了重排序,加鎖後重新讀取
                        return readValueUnderLock(e); 
                    } 
                    e = e.next; 
                } 
            } 
            return null; 
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

ConcurrentHashMap中的讀方法不需要加鎖,所有的修改操作在進行結構修改時都會在最後一步寫count 變數,通過這種機制保證get操作能夠得到幾乎最新的結構更新。

(3)Remove方法的實現
V remove(Object key, int hash, Object value) { 
            lock(); //加鎖
            try{ 
                int c = count - 1; 
                HashEntry<K,V>[] tab = table; 
                //根據雜湊碼找到 table 的下標值
                int index = hash & (tab.length - 1); 
                //找到雜湊碼對應的那個桶
                HashEntry<K,V> first = tab[index]; 
                HashEntry<K,V> e = first; 
                while(e != null&& (e.hash != hash || !key.equals(e.key))) 
                    e = e.next; 

                V oldValue = null; 
                if(e != null) { 
                    V v = e.value; 
                    if(value == null|| value.equals(v)) { //找到要刪除的節點
                        oldValue = v; 
                        ++modCount; 
                        //所有處於待刪除節點之後的節點原樣保留在連結串列中
                        //所有處於待刪除節點之前的節點被克隆到新連結串列中
                        HashEntry<K,V> newFirst = e.next;// 待刪節點的後繼結點
                        for(HashEntry<K,V> p = first; p != e; p = p.next) 
                            newFirst = new HashEntry<K,V>(p.key, p.hash, 
                                                          newFirst, p.value); 
                        //把桶連結到新的頭結點
                        //新的頭結點是原連結串列中,刪除節點之前的那個節點
                        tab[index] = newFirst; 
                        count = c;      //寫 count 變數
                    } 
                } 
                return oldValue; 
            } finally{ 
                unlock(); //解鎖
            } 
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

整個操作是在持有段鎖的情況下執行的,空白行之前的行主要是定位到要刪除的節點e。接下來,如果不存在這個節點就直接返回null,否則就要將e前面的結點複製一遍,尾結點指向e的下一個結點。e後面的結點不需要複製,它們可以重用。

中間那個for迴圈是做什麼用的呢?從程式碼來看,就是將定位之後的所有entry克隆並拼回前面去,但有必要嗎?每次刪除一個元素就要將那之前的元素克隆一遍?這點其實是由entry的不變性來決定的,仔細觀察entry定義,發現除了value,其他所有屬性都是用final來修飾的,這意味著在第一次設定了next域之後便不能再改變它,取而代之的是將它之前的節點全都克隆一次。至於entry為什麼要設定為不變性,這跟不變性的訪問不需要同步從而節省時間有關。

執行刪除之前的原連結串列: 
這裡寫圖片描述

執行刪除之後的新連結串列 
這裡寫圖片描述

注意:新連結串列在clone的時候。順序發生反轉,A->B變為B->A。

(4)containsKey方法的實現

containsKey方法操作相對簡單,因為它不需要讀取值。

boolean containsKey(Object key, int hash) {  
     if (count != 0) { // read-volatile  
         HashEntry<K,V> e = getFirst(hash);  
         while (e != null) {  
             if (e.hash == hash && key.equals(e.key))  
                 return true;  
             e = e.next;  
         }  
     }  
     return false;  
 } 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

4.總結

在使用鎖來協調多執行緒間併發訪問的模式下,減小對鎖的競爭可以有效提高併發性。有兩種方式可以減小對鎖的競爭:

  • 減小請求同一個鎖的頻率。
  • 減少持有鎖的時間。

ConcurrentHashMap 的高併發性主要來自於三個方面:

  • 用分離鎖實現多個執行緒間的更深層次的共享訪問。
  • 用 HashEntery 物件的不變性來降低執行讀操作的執行緒在遍歷連結串列期間對加鎖的需求。
  • 通過對同一個 Volatile 變數的寫 / 讀訪問,協調不同執行緒間讀 / 寫操作的記憶體可見性。

使用分離鎖,減小了請求同一個鎖的頻率。

相關文章