探索 ConcurrentHashMap 高併發性的實現機制

原文:http://www.ibm.com/developerworks/cn/java/java-lo-concurrenthashmap/


簡介

ConcurrentHashMap 是 util.concurrent 包的重要成員。本文將結合 Java 內存模型,分析 JDK 源代碼,探索 ConcurrentHashMap 高併發的具體實現機制。

由於 ConcurrentHashMap 的源代碼實現依賴於 Java 內存模型,所以閱讀本文需要讀者瞭解 Java 內存模型。同時,ConcurrentHashMap 的源代碼會涉及到散列算法和鏈表數據結構,所以,讀者需要對散列算法和基於鏈表的數據結構有所瞭解。

Java 內存模型

由於 ConcurrentHashMap 是建立在 Java 內存模型基礎上的,爲了更好的理解 ConcurrentHashMap,讓我們首先來了解一下 Java 的內存模型。

Java 語言的內存模型由一些規則組成,這些規則確定線程對內存的訪問如何排序以及何時可以確保它們對線程是可見的。下面我們將分別介紹 Java 內存模型的重排序,內存可見性和 happens-before 關係。

重排序

內存模型描述了程序的可能行爲。具體的編譯器實現可以產生任意它喜歡的代碼 -- 只要所有執行這些代碼產生的結果,能夠和內存模型預測的結果保持一致。這爲編譯器實現者提供了很大的自由,包括操作的重排序。

編譯器生成指令的次序,可以不同於源代碼所暗示的“顯然”版本。重排序後的指令,對於優化執行以及成熟的全局寄存器分配算法的使用,都是大有脾益的,它使得程序在計算性能上有了很大的提升。

重排序類型包括:

  • 編譯器生成指令的次序,可以不同於源代碼所暗示的“顯然”版本。
  • 處理器可以亂序或者並行的執行指令。
  • 緩存會改變寫入提交到主內存的變量的次序。

內存可見性

由於現代可共享內存的多處理器架構可能導致一個線程無法馬上(甚至永遠)看到另一個線程操作產生的結果。所以 Java 內存模型規定了 JVM 的一種最小保證:什麼時候寫入一個變量對其他線程可見。

在現代可共享內存的多處理器體系結構中每個處理器都有自己的緩存,並週期性的與主內存協調一致。假設線程 A 寫入一個變量值 V,隨後另一個線程 B 讀取變量 V 的值,在下列情況下,線程 B 讀取的值可能不是線程 A 寫入的最新值:

  • 執行線程 A 的處理器把變量 V 緩存到寄存器中。
  • 執行線程 A 的處理器把變量 V 緩存到自己的緩存中,但還沒有同步刷新到主內存中去。
  • 執行線程 B 的處理器的緩存中有變量 V 的舊值。

Happens-before 關係

happens-before 關係保證:如果線程 A 與線程 B 滿足 happens-before 關係,則線程 A 執行動作的結果對於線程 B 是可見的。如果兩個操作未按 happens-before 排序,JVM 將可以對他們任意重排序。

下面介紹幾個與理解 ConcurrentHashMap 有關的 happens-before 關係法則:

  1. 程序次序法則:如果在程序中,所有動作 A 出現在動作 B 之前,則線程中的每動作 A 都 happens-before 於該線程中的每一個動作 B。
  2. 監視器鎖法則:對一個監視器的解鎖 happens-before 於每個後續對同一監視器的加鎖。
  3. Volatile 變量法則:對 Volatile 域的寫入操作 happens-before 於每個後續對同一 Volatile 的讀操作。
  4. 傳遞性:如果 A happens-before 於 B,且 B happens-before C,則 A happens-before C。

ConcurrentHashMap 的結構分析

爲了更好的理解 ConcurrentHashMap 高併發的具體實現,讓我們先探索它的結構模型。

ConcurrentHashMap 類中包含兩個靜態內部類 HashEntry 和 Segment。HashEntry 用來封裝映射表的鍵 / 值對;Segment 用來充當鎖的角色,每個 Segment 對象守護整個散列映射表的若干個桶。每個桶是由若干個 HashEntry 對象鏈接起來的鏈表。一個 ConcurrentHashMap 實例中包含由若干個 Segment 對象組成的數組。

HashEntry 類

HashEntry 用來封裝散列映射表中的鍵值對。在 HashEntry 類中,key,hash 和 next 域都被聲明爲 final 型,value 域被聲明爲 volatile 型。


清單 1.HashEntry 類的定義
				 
 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; 
        } 
 } 


在 ConcurrentHashMap 中,在散列時如果產生“碰撞”,將採用“分離鏈接法”來處理“碰撞”:把“碰撞”的 HashEntry 對象鏈接成一個鏈表。由於 HashEntry 的 next 域爲 final 型,所以新節點只能在鏈表的表頭處插入。 下圖是在一個空桶中依次插入 A,B,C 三個 HashEntry 對象後的結構圖:


圖 1. 插入三個節點後桶的結構示意圖:
圖 1. 插入三個節點後桶的結構示意圖: 

注意:由於只能在表頭插入,所以鏈表中節點的順序和插入的順序相反。

避免熱點域

在 ConcurrentHashMap中,每一個 Segment 對象都有一個 count 對象來表示本 Segment 中包含的 HashEntry 對象的個數。這樣當需要更新計數器時,不用鎖定整個ConcurrentHashMap

Segment 類

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

table 是一個由 HashEntry 對象組成的數組。table 數組的每一個數組成員就是散列映射表的一個桶。

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


清單 2.Segment 類的定義
				 
 static final class Segment<K,V> extends ReentrantLock implements Serializable { 
        /** 
         * 在本 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; 

        Segment(int initialCapacity, float lf) { 
            loadFactor = lf; 
            setTable(HashEntry.<K,V>newArray(initialCapacity)); 
        } 

        /** 
         * 設置 table 引用到這個新生成的 HashEntry 數組
         * 只能在持有鎖或構造函數中調用本方法
         */ 
        void setTable(HashEntry<K,V>[] newTable) { 
            // 計算臨界閥值爲新數組的長度與裝載因子的乘積
            threshold = (int)(newTable.length * loadFactor); 
            table = newTable; 
        } 

        /** 
         * 根據 key 的散列值,找到 table 中對應的那個桶(table 數組的某個數組成員)
         */ 
        HashEntry<K,V> getFirst(int hash) { 
            HashEntry<K,V>[] tab = table; 
            // 把散列值與 table 數組長度減 1 的值相“與”,
 // 得到散列值對應的 table 數組的下標
            // 然後返回 table 數組中此下標對應的 HashEntry 元素
            return tab[hash & (tab.length - 1)]; 
        } 
 } 


下圖是依次插入 ABC 三個 HashEntry 節點後,Segment 的結構示意圖。


圖 2. 插入三個節點後 Segment 的結構示意圖:
圖 2. 插入三個節點後 Segment 的結構示意圖: 

ConcurrentHashMap 類

ConcurrentHashMap 在默認併發級別會創建包含 16 個 Segment 對象的數組。每個 Segment 的成員對象 table 包含若干個散列表的桶。每個桶是由 HashEntry 鏈接起來的一個鏈表。如果鍵能均勻散列,每個 Segment 大約守護整個散列表中桶總數的 1/16。


清單 3.ConcurrentHashMap 類的定義
				 
 public class ConcurrentHashMap<K, V> extends AbstractMap<K, V> 
        implements ConcurrentMap<K, V>, Serializable { 

    /** 
     * 散列映射表的默認初始容量爲 16,即初始默認爲 16 個桶
     * 在構造函數中沒有指定這個參數時,使用本參數
     */ 
    static final 	 int DEFAULT_INITIAL_CAPACITY= 16; 

    /** 
     * 散列映射表的默認裝載因子爲 0.75,該值是 table 中包含的 HashEntry 元素的個數與
 * table 數組長度的比值
     * 當 table 中包含的 HashEntry 元素的個數超過了 table 數組的長度與裝載因子的乘積時,
 * 將觸發 再散列
     * 在構造函數中沒有指定這個參數時,使用本參數
     */ 
    static final float DEFAULT_LOAD_FACTOR= 0.75f; 

    /** 
     * 散列表的默認併發級別爲 16。該值表示當前更新線程的估計數
     * 在構造函數中沒有指定這個參數時,使用本參數
     */ 
    static final int DEFAULT_CONCURRENCY_LEVEL= 16; 

    /** 
     * segments 的掩碼值
     * key 的散列碼的高位用來選擇具體的 segment 
     */ 
    final int segmentMask; 

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

    /** 
     * 由 Segment 對象組成的數組
     */ 
    final Segment<K,V>[] segments; 

    /** 
     * 創建一個帶有指定初始容量、加載因子和併發級別的新的空映射。
     */ 
    public ConcurrentHashMap(int initialCapacity, 
                             float loadFactor, int concurrencyLevel) { 
        if(!(loadFactor > 0) || initialCapacity < 0 || 
 concurrencyLevel <= 0) 
            throw new IllegalArgumentException(); 

        if(concurrencyLevel > MAX_SEGMENTS) 
            concurrencyLevel = MAX_SEGMENTS; 

        // 尋找最佳匹配參數(不小於給定參數的最接近的 2 次冪) 
        int sshift = 0; 
        int ssize = 1; 
        while(ssize < concurrencyLevel) { 
            ++sshift; 
            ssize <<= 1; 
        } 
        segmentShift = 32 - sshift;       // 偏移量值
        segmentMask = ssize - 1;           // 掩碼值 
        this.segments = Segment.newArray(ssize);   // 創建數組

        if (initialCapacity > MAXIMUM_CAPACITY) 
            initialCapacity = MAXIMUM_CAPACITY; 
        int c = initialCapacity / ssize; 
        if(c * ssize < initialCapacity) 
            ++c; 
        int cap = 1; 
        while(cap < c) 
            cap <<= 1; 

        // 依次遍歷每個數組元素
        for(int i = 0; i < this.segments.length; ++i) 
            // 初始化每個數組元素引用的 Segment 對象
 this.segments[i] = new Segment<K,V>(cap, loadFactor); 
    } 

    /** 
     * 創建一個帶有默認初始容量 (16)、默認加載因子 (0.75) 和 默認併發級別 (16) 
  * 的空散列映射表。
     */ 
    public ConcurrentHashMap() { 
        // 使用三個默認參數,調用上面重載的構造函數來創建空散列映射表
 this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL); 
 } 

}

下面是 ConcurrentHashMap 的結構示意圖。


圖 3.ConcurrentHashMap 的結構示意圖:
圖 3.ConcurrentHashMap 的結構示意圖: 

用分離鎖實現多個線程間的併發寫操作

在 ConcurrentHashMap 中,線程對映射表做讀操作時,一般情況下不需要加鎖就可以完成,對容器做結構性修改的操作才需要加鎖。下面以 put 操作爲例說明對 ConcurrentHashMap 做結構性修改的過程。

首先,根據 key 計算出對應的 hash 值:


清單 4.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); 
 } 

然後,根據 hash 值找到對應的Segment 對象:


清單 5.根據 hash 值找到對應的 Segment
				 
 /** 
     * 使用 key 的散列碼來得到 segments 數組中對應的 Segment 
     */ 
 final Segment<K,V> segmentFor(int hash) { 
    // 將散列值右移 segmentShift 個位,並在高位填充 0 
    // 然後把得到的值與 segmentMask 相“與”
 // 從而得到 hash 值對應的 segments 數組的下標值
 // 最後根據下標值返回散列碼對應的 Segment 對象
        return segments[(hash >>> segmentShift) & segmentMask]; 
 } 


最後,在這個 Segment 中執行具體的 put 操作:


清單 6.在 Segment 中執行具體的 put 操作
				 
 V put(K key, int hash, V value, boolean onlyIfAbsent) { 
            lock();  // 加鎖,這裏是鎖定某個 Segment 對象而非整個 ConcurrentHashMap 
            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();                     // 解鎖
            } 
        } 

注意:這裏的加鎖操作是針對(鍵的 hash 值對應的)某個具體的 Segment,鎖定的是該 Segment 而不是整個 ConcurrentHashMap。因爲插入鍵 / 值對操作只是在這個 Segment 包含的某個桶中完成,不需要鎖定整個ConcurrentHashMap。此時,其他寫線程對另外 15 個Segment 的加鎖並不會因爲當前線程對這個 Segment 的加鎖而阻塞。同時,所有讀線程幾乎不會因本線程的加鎖而阻塞(除非讀線程剛好讀到這個 Segment 中某個 HashEntry 的 value 域的值爲 null,此時需要加鎖後重新讀取該值)。

相比較於 HashTable 和由同步包裝器包裝的 HashMap每次只能有一個線程執行讀或寫操作,ConcurrentHashMap 在併發訪問性能上有了質的提高。在理想狀態下,ConcurrentHashMap 可以支持 16 個線程執行併發寫操作(如果併發級別設置爲 16),及任意數量線程的讀操作。

用 HashEntery 對象的不變性來降低讀操作對加鎖的需求

在代碼清單“HashEntry 類的定義”中我們可以看到,HashEntry 中的 key,hash,next 都聲明爲 final 型。這意味着,不能把節點添加到鏈接的中間和尾部,也不能在鏈接的中間和尾部刪除節點。這個特性可以保證:在訪問某個節點時,這個節點之後的鏈接不會被改變。這個特性可以大大降低處理鏈表時的複雜性。

同時,HashEntry 類的 value 域被聲明爲 Volatile 型,Java 的內存模型可以保證:某個寫線程對 value 域的寫入馬上可以被後續的某個讀線程“看”到。在 ConcurrentHashMap 中,不允許用 unll 作爲鍵和值,當讀線程讀到某個 HashEntry 的 value 域的值爲 null 時,便知道產生了衝突——發生了重排序現象,需要加鎖後重新讀入這個 value 值。這些特性互相配合,使得讀線程即使在不加鎖狀態下,也能正確訪問 ConcurrentHashMap。

下面我們分別來分析線程寫入的兩種情形:對散列表做非結構性修改的操作和對散列表做結構性修改的操作。

非結構性修改操作只是更改某個 HashEntry 的 value 域的值。由於對 Volatile 變量的寫入操作將與隨後對這個變量的讀操作進行同步。當一個寫線程修改了某個 HashEntry 的 value 域後,另一個讀線程讀這個值域,Java 內存模型能夠保證讀線程讀取的一定是更新後的值。所以,寫線程對鏈表的非結構性修改能夠被後續不加鎖的讀線程“看到”。

對 ConcurrentHashMap 做結構性修改,實質上是對某個桶指向的鏈表做結構性修改。如果能夠確保:在讀線程遍歷一個鏈表期間,寫線程對這個鏈表所做的結構性修改不影響讀線程繼續正常遍歷這個鏈表。那麼讀 / 寫線程之間就可以安全併發訪問這個 ConcurrentHashMap。

結構性修改操作包括 put,remove,clear。下面我們分別分析這三個操作。

clear 操作只是把 ConcurrentHashMap 中所有的桶“置空”,每個桶之前引用的鏈表依然存在,只是桶不再引用到這些鏈表(所有鏈表的結構並沒有被修改)。正在遍歷某個鏈表的讀線程依然可以正常執行對該鏈表的遍歷。

從上面的代碼清單“在 Segment 中執行具體的 put 操作”中,我們可以看出:put 操作如果需要插入一個新節點到鏈表中時 , 會在鏈表頭部插入這個新節點。此時,鏈表中的原有節點的鏈接並沒有被修改。也就是說:插入新健 / 值對到鏈表中的操作不會影響讀線程正常遍歷這個鏈表。

下面來分析 remove 操作,先讓我們來看看 remove 操作的源代碼實現。


清單 7.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();               // 解鎖
            } 
        } 

和 get 操作一樣,首先根據散列碼找到具體的鏈表;然後遍歷這個鏈表找到要刪除的節點;最後把待刪除節點之後的所有節點原樣保留在新鏈表中,把待刪除節點之前的每個節點克隆到新鏈表中。下面通過圖例來說明 remove 操作。假設寫線程執行 remove 操作,要刪除鏈表的 C 節點,另一個讀線程同時正在遍歷這個鏈表。


圖 4. 執行刪除之前的原鏈表:
圖 4. 執行刪除之前的原鏈表: 

圖 5. 執行刪除之後的新鏈表
圖 5. 執行刪除之後的新鏈表 

從上圖可以看出,刪除節點 C 之後的所有節點原樣保留到新鏈表中;刪除節點 C 之前的每個節點被克隆到新鏈表中,注意:它們在新鏈表中的鏈接順序被反轉了

在執行 remove 操作時,原始鏈表並沒有被修改,也就是說:讀線程不會受同時執行 remove 操作的併發寫線程的干擾。

綜合上面的分析我們可以看出,寫線程對某個鏈表的結構性修改不會影響其他的併發讀線程對這個鏈表的遍歷訪問。

用 Volatile 變量協調讀寫線程間的內存可見性

由於內存可見性問題,未正確同步的情況下,寫線程寫入的值可能並不爲後續的讀線程可見。

下面以寫線程 M 和讀線程 N 來說明 ConcurrentHashMap 如何協調讀 / 寫線程間的內存可見性問題。


圖 6. 協調讀 - 寫線程間的內存可見性的示意圖:
圖 6. 協調讀 - 寫線程間的內存可見性的示意圖: 

假設線程 M 在寫入了 volatile 型變量 count 後,線程 N 讀取了這個 volatile 型變量 count。

根據 happens-before 關係法則中的程序次序法則,A appens-before 於 B,C happens-before D。

根據 Volatile 變量法則,B happens-before C。

根據傳遞性,連接上面三個 happens-before 關係得到:A appens-before 於 B; B appens-before C;C happens-before D。也就是說:寫線程 M 對鏈表做的結構性修改,在讀線程 N 讀取了同一個 volatile 變量後,對線程 N 也是可見的了。

雖然線程 N 是在未加鎖的情況下訪問鏈表。Java 的內存模型可以保證:只要之前對鏈表做結構性修改操作的寫線程 M 在退出寫方法前寫 volatile 型變量 count,讀線程 N 在讀取這個 volatile 型變量 count 後,就一定能“看到”這些修改。

ConcurrentHashMap 中,每個 Segment 都有一個變量 count。它用來統計 Segment 中的 HashEntry 的個數。這個變量被聲明爲 volatile。


清單 8.Count 變量的聲明
				 
 transient volatile int count; 



所有不加鎖讀方法,在進入讀方法時,首先都會去讀這個 count 變量。比如下面的 get 方法:


清單 9.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; 
        } 

在 ConcurrentHashMap 中,所有執行寫操作的方法(put, remove, clear),在對鏈表做結構性修改之後,在退出寫方法前都會去寫這個 count 變量。所有未加鎖的讀操作(get, contains, containsKey)在讀方法中,都會首先去讀取這個 count 變量。

根據 Java 內存模型,對 同一個 volatile 變量的寫 / 讀操作可以確保:寫線程寫入的值,能夠被之後未加鎖的讀線程“看到”。

這個特性和前面介紹的 HashEntry 對象的不變性相結合,使得在 ConcurrentHashMap 中,讀線程在讀取散列表時,基本不需要加鎖就能成功獲得需要的值。這兩個特性相配合,不僅減少了請求同一個鎖的頻率(讀操作一般不需要加鎖就能夠成功獲得值),也減少了持有同一個鎖的時間(只有讀到 value 域的值爲 null 時 , 讀線程才需要加鎖後重讀)。

ConcurrentHashMap 實現高併發的總結

基於通常情形而優化

在實際的應用中,散列表一般的應用場景是:除了少數插入操作和刪除操作外,絕大多數都是讀取操作,而且讀操作在大多數時候都是成功的。正是基於這個前提,ConcurrentHashMap 針對讀操作做了大量的優化。通過 HashEntry 對象的不變性和用 volatile 型變量協調線程間的內存可見性,使得 大多數時候,讀操作不需要加鎖就可以正確獲得值。這個特性使得 ConcurrentHashMap 的併發性能在分離鎖的基礎上又有了近一步的提高。

總結

ConcurrentHashMap 是一個併發散列映射表的實現,它允許完全併發的讀取,並且支持給定數量的併發更新。相比於 HashTable 和用同步包裝器包裝的 HashMap(Collections.synchronizedMap(new HashMap())),ConcurrentHashMap 擁有更高的併發性。在 HashTable 和由同步包裝器包裝的 HashMap 中,使用一個全局的鎖來同步不同線程間的併發訪問。同一時間點,只能有一個線程持有鎖,也就是說在同一時間點,只能有一個線程能訪問容器。這雖然保證多線程間的安全併發訪問,但同時也導致對容器的訪問變成串行化的了。

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

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

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

  1. 用分離鎖實現多個線程間的更深層次的共享訪問。
  2. 用 HashEntery 對象的不變性來降低執行讀操作的線程在遍歷鏈表期間對加鎖的需求。
  3. 通過對同一個 Volatile 變量的寫 / 讀訪問,協調不同線程間讀 / 寫操作的內存可見性。

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

通過 HashEntery 對象的不變性及對同一個 Volatile 變量的讀 / 寫來協調內存可見性,使得 讀操作大多數時候不需要加鎖就能成功獲取到需要的值。由於散列映射表在實際應用中大多數操作都是成功的 讀操作,所以 2 和 3 既可以減少請求同一個鎖的頻率,也可以有效減少持有鎖的時間。

通過減小請求同一個鎖的頻率和儘量減少持有鎖的時間 ,使得 ConcurrentHashMap 的併發性相對於 HashTable 和用同步包裝器包裝的 HashMap有了質的提高。


參考資料

學習

  • Java 語言規範(第 3 版):Java 程序設計語言最權威的技術參考書。17.4 章探討了 Java 內存模型。 

  • Java 併發編程實踐:本書作者系 Java 標準化組織 JSR 166 專家組的主要成員,本書是近年來 Java 併發編程圖書中最值得一讀的力作。5.2 章探討了併發容器, 11.4 探討了如何減少鎖的競爭,11.5 比較了各種 Map 的性能,16 章探討了 Java 內存模型。 

  • Java 併發編程—設計原則與模式(第二版):Java 併發編程領域的先驅 --Doug Lea 先生的經典著作。Doug Lea 先生是 JDK 中 util.concurrent 包的實現者。 

  • 多核系統的 Java 併發缺陷模式(bug patterns):本文通過對 6 個鮮爲人知的併發缺陷問題的講解,闡述了威脅運行在多核系統上的 Java 應用程序線程安全和性能的原因,同時帶領您研究併發缺陷模式(bug patterns),讓您既能夠提高對併發編程的理解,還能夠了解如何發現無效或可能無效的編程方法。

  • Java 多線程與併發編程專題:本專題彙集了與 Java 多線程與併發編程相關的文章和教程,幫助讀者理解 Java 併發編程的模式及其利弊,向讀者展示瞭如何更精確地使用 Java 平臺的線程模型。 

  • developerWorks Java 技術專區:這裏有數百篇關於 Java 編程各個方面的文章。 

討論

  • 加入 developerWorks 中文社區。查看開發人員推動的博客、論壇、組和維基,並與其他 developerWorks 用戶交流。

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