ConcurrentHashMap實現機制

Java 內存模型

因爲 ConcurrentHashMap 是創建在 Java 內存模型基礎上的,爲了更好的理解 ConcurrentHashMap,讓咱們首先來了解一下 Java 的內存模型。html

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

重排序

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

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

重排序類型包括:數組

  • 編譯器生成指令的次序,能夠不一樣於源代碼所暗示的「顯然」版本。緩存

  • 處理器能夠亂序或者並行的執行指令。安全

  • 緩存會改變寫入提交到主內存的變量的次序。多線程

內存可見性

因爲現代可共享內存的多處理器架構可能致使一個線程沒法立刻(甚至永遠)看到另外一個線程操做產生的結果。因此 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有了質的提升。

相關文章
相關標籤/搜索