2.3、JDK 源碼分析 - ConcurrentHashMap1.7

摘要

我們都知道HashMap是線程不安全的,擴容時有可能還會產生死循環!那麼有沒有一種比較安全的HashMap給我們使用呢?JDK其實已經爲我們提供了一種實現,它就是ConcurrentHashMap;

介紹

一個支持檢索的完全併發性和更新的可調預期併發性的哈希表。 這個類遵守與Hashtable相同的功能規範,幷包含與Hashtable的每個方法對應的方法版本。 然而,即使所有操作都是線程安全的,檢索操作也不需要鎖定,並且不支持以阻止所有訪問的方式鎖定整個表。 在依賴Hashtable的線程安全性但不依賴其同步細節的程序中,這個類與Hashtable完全可互操作。

檢索操作(包括get)通常不會阻塞,因此可能與更新操作(包括put和remove)重疊。 檢索反映了最近完成的更新操作在開始時保持的結果。 對於聚合操作(如putAll和clear),併發檢索可能只反映某些條目的插入或刪除。 類似地,Iterators和Enumerations返回的元素反映了哈希表在創建迭代器/枚舉時的某個時點或自創建以來的狀態。 它們不會拋出ConcurrentModificationException。 但是,迭代器被設計爲一次只能被一個線程使用。

更新操作之間允許的併發性由可選的concurrencyLevel構造函數參數(默認16)指導,該參數被用作內部大小調整的提示。 對錶進行內部分區,以嘗試在沒有爭用的情況下允許指定數量的併發更新。 因爲哈希表中的位置基本上是隨機的,所以實際的併發性會有所不同。 理想情況下,您應該選擇一個值來容納儘可能多的併發修改表的線程。 使用比您需要的高得多的值可能會浪費空間和時間,而使用較低的值可能會導致線程爭用。 但是在一個數量級內的高估和低估通常不會產生太明顯的影響。 當知道只有一個線程會修改,而其他所有線程只會讀取時,值1是合適的。 另外,調整這個或任何其他類型的散列表的大小是一個相對較慢的操作,因此,如果可能的話,最好在構造函數中提供預期表大小的估計。

這個類及其視圖和迭代器實現了Map和Iterator接口的所有可選方法。 與Hashtable類似但又不同於HashMap,該類不允許將null用作鍵或值。

基本策略是將表細分爲Segments,每個Segments本身是一個併發可讀的哈希表。 爲了減少內存佔用,除了一個段之外的所有段只在第一次需要時才構造(參見ensureSegment)。 爲了在惰性構造的情況下保持可見性,訪問段以及段表的元素必須使用volatile訪問,這是通過不安全的方法segmentAt等完成的。 它們提供了AtomicReferenceArrays的功能,但減少了間接級別。 另外,鎖操作中對錶元素和條目“next”字段的volatile寫操作使用更便宜的“lazySet”形式(通過putOrderedObject),因爲這些寫操作之後總是會釋放鎖,以保持表更新的順序一致性。

歷史提示:該類的上一個版本嚴重依賴於“final”字段,這避免了一些volatile讀取,但代價是大量的初始佔用空間。 該設計的一些殘餘(包括強制構造段0)存在以確保串行兼容性。

源碼解析

(1)、類定義

public class ConcurrentHashMap<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V>, Serializable {
   ...
}

類定義沒有什麼特別之處,就是實現ConcurrentMap接口,在ConcurrentMap定義了幾個原子方法

(2)、常量定義

    /**
     * 該表的默認初始容量,當沒有在構造中指定時使用
     */
    static final int DEFAULT_INITIAL_CAPACITY = 16;

    /**
     * 該表的默認加載因子,當沒有在構造函數中指定時使用。
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * 該表的默認併發級別,當沒有在構造函數中指定時使用。
     */
    static final int DEFAULT_CONCURRENCY_LEVEL = 16;

    /**
     * 最大容量,如果一個較大的值由帶參數的構造函數中的任何一個隱式指定,則使用該值。 必須是2的冪<= 1<<30,以確保條目可以使用整數進行索引。
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * 每個段表的最小容量。 必須是2的冪,至少爲2,以避免在惰性構造後的下一次使用中立即調整大小。
     */
    static final int MIN_SEGMENT_TABLE_CAPACITY = 2;

    /**
     * 允許的最大段數; 用於綁定構造函數參數。 必須是2的冪,小於1 << 24。
     */
    static final int MAX_SEGMENTS = 1 << 16; // slightly conservative  略顯保守

    /**
     * 在訴諸鎖定之前,在大小和containsValue方法上的非同步重試次數。 這用於避免在表經歷連續修改時進行無界重試,這將導致無法獲得準確的結果。
     */
    static final int RETRIES_BEFORE_LOCK = 2;

(3)、字段定義

    //與此實例關聯的隨機值,應用於鍵的散列代碼,使散列衝突更難找到。
    private transient final int hashSeed = randomHashSeed(this);

    /**
     * Mask value for indexing into segments. The upper bits of a
     * key's hash code are used to choose the segment.
     * 索引段的掩碼值。 鍵的哈希碼的上位用於選擇段。
     */
    final int segmentMask;

    /**
     * 在段內移位索引值。
     */
    final int segmentShift;

    /**
     * 每個段都是一個專門的哈希表。用於存放真實數據
     */
    final Segment<K,V>[] segments;

    transient Set<K> keySet;
    transient Set<Map.Entry<K,V>> entrySet;
    transient Collection<V> values;

(4)、內部類HashEntry

    /**
     * ConcurrentHashMap列表條目。 請注意,它從未作爲用戶可見的Map.Entry導出。
     */
    static final class HashEntry<K,V> {
        final int hash;
        final K key;
        volatile V value;
        volatile HashEntry<K,V> next;

        HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        /**
         * 用volatile寫語義設置下一個字段。 (請參閱上面關於putOrderedObject的使用。)
         */
        final void setNext(HashEntry<K,V> n) {
            UNSAFE.putOrderedObject(this, nextOffset, n);
        }

        // Unsafe mechanics
        static final sun.misc.Unsafe UNSAFE;
        static final long nextOffset;
        static {
            try {
                UNSAFE = sun.misc.Unsafe.getUnsafe();
                Class k = HashEntry.class;
                nextOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("next"));
            } catch (Exception e) {
                throw new Error(e);
            }
        }
    }

(6)、構造方法

    /**
     * 創建一個新的空映射,帶有指定的初始容量,以及默認的負載因子(0.75)和concurrencyLevel(16)。
     */
    public ConcurrentHashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
    }

    /**
     * 創建一個新的空映射,具有默認的初始容量(16)、負載因子(0.75)和併發級別(16)。
     */
    public ConcurrentHashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
    }

    /**
     * 創建具有與給定映射相同映射的新映射。 創建的映射的容量是給定映射的1.5倍或16(哪個更大),以及默認的負載因子(0.75)和concurrencyLevel (16)
     */
    public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                      DEFAULT_INITIAL_CAPACITY),
             DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
        putAll(m);
    }
    /**
     * 使用指定的初始容量、負載因子和併發級別創建一個新的空映射。
     *
     * @param initialCapacity 最初的能力。 該實現執行內部大小調整以適應這麼多元素。
     * @param loadFactor 負載因子閾值,用於控制調整容量大小。當元素的平均數量超過這個閾值時,可以執行調整大小。
     * @param concurrencyLevel 併發更新線程的估計數目。 該實現執行內部大小調整以嘗試容納這麼多線程。
     */
    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;
        // Find power-of-two sizes best matching arguments 找到兩倍大小的最佳匹配參數
        int sshift = 0;
        int ssize = 1;
        while (ssize < concurrencyLevel) {
            ++sshift;
            ssize <<= 1;
        }
        this.segmentShift = 32 - sshift;
        this.segmentMask = ssize - 1;
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        int c = initialCapacity / ssize;
        if (c * ssize < initialCapacity)
            ++c;
        int cap = MIN_SEGMENT_TABLE_CAPACITY;
        while (cap < c)
            cap <<= 1;
        // create segments and segments[0]
        Segment<K,V> s0 =new Segment<K,V>(loadFactor, (int)(cap * loadFactor), (HashEntry<K,V>[])new HashEntry[cap]);
        Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
        UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
        this.segments = ss;
    }

(7)、內部類Segment

段是哈希表的特殊版本。 這個子類取巧地繼承了ReentrantLock,只是爲了簡化一些鎖並避免單獨構造。

段維護一個始終保持一致狀態的條目列表表,所以可以在不鎖定的情況下讀取(通過對段和表的volatile讀取)。 這需要在調整表大小時複製節點,因此仍然使用舊版本表的讀者可以遍歷舊列表。

該類只定義需要鎖定的可變方法。 除了前面提到的,這個類的方法執行ConcurrentHashMap方法的每段版本。 (其他方法直接集成到ConcurrentHashMap方法中。) 這些可變的方法通過scanAndLock和scanAndLockForPut方法在爭用時使用一種控制旋轉的形式。 這些函數將trylock與遍歷穿插在一起以定位節點。 它的主要好處是在獲取鎖時吸收緩存丟失(這在散列表中很常見),這樣一旦獲得鎖,遍歷就會更快。 我們實際上並不使用找到的節點,因爲它們必須在鎖定狀態下重新獲取,以確保更新的順序一致性(在任何情況下都可能無法檢測到過時),但它們通常會更快地重新定位。 此外,如果沒有找到節點,scanAndLockForPut投機性地創建一個新的節點用於put。

    static final class Segment<K,V> extends ReentrantLock implements Serializable {
        /**
         * 預掃描中在可能阻塞之前嘗試鎖定的最大次數,爲鎖定段操作做準備。 在多處理器上,使用有限的重試次數來維護定位節點時獲得的緩存。
         */
        static final int MAX_SCAN_RETRIES = Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;

        /**
         *  每段的hash表。 通過提供volatile語義的entryAt/setEntryAt訪問元素。
         */
        transient volatile HashEntry<K,V>[] table;

        /**
         * 元素的數量。 只能在鎖中或在其他保持可見性的volatile讀取中訪問。
         */
        transient int count;

        /**
         * 在這個段中發生變化的操作的總數。 儘管這可能會溢出32位,但它爲CHM isEmpty()和size()方法中的穩定性檢查提供了足夠的準確性。 
         * 只能在鎖中或在其他保持可見性的volatile讀取中訪問。
         */
        transient int modCount;

        /**
         * 當表的大小超過此閾值時,表將重新散列。 (該字段的值總是(int)(capacity * loadFactor)。)
         */
        transient int threshold;

        /**
         * 哈希表的負載因子。 即使這個值對於所有的段都是相同的,它也會被複制以避免需要連接到外部對象。
         */
        final float loadFactor;

        Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
            this.loadFactor = lf;
            this.threshold = threshold;
            this.table = tab;
        }
        ....
    }

從上面可以看出分段,這裏面的段 其實是一個個鎖。

(8)、get方法

    /**
     * 返回指定鍵映射到的值,如果這個映射不包含該鍵的映射,則返回{@code null}。
     */
    public V get(Object key) {
        Segment<K,V> s;
        HashEntry<K,V>[] tab;
        int h = hash(key);
       //定位段地址
        long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
        //通過地址獲取內存可見的分段
        if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&(tab = s.table) != null) {
            for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);  e != null; e = e.next) {
                K k;
                if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                    return e.value;
            }
        }
        return null;
    }

(9)、put方法

    /**
     * 將指定的鍵映射到該表中的指定值。 鍵和值都不能爲空。
     *
     * 可以通過調用get方法來檢索該值,該方法使用的鍵等於原始鍵。
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return 前一個值與鍵關聯,或null(如果鍵沒有映射的話)
     * @throws NullPointerException 如果指定的鍵或值爲空
     */
    public V put(K key, V value) {
        Segment<K,V> s;
        //如果值爲空則拋出異常
        if (value == null)
            throw new NullPointerException();
        //如果值爲空,該方法也會拋出異常
        int hash = hash(key);
        int j = (hash >>> segmentShift) & segmentMask;
        // nonvolatile; recheck
        if ((s = (Segment<K,V>)UNSAFE.getObject (segments, (j << SSHIFT) + SBASE)) == null)
            //  in ensureSegment
            s = ensureSegment(j);
        return s.put(key, hash, value, false);
    }

調用分段的put方法

        final V put(K key, int hash, V value, boolean onlyIfAbsent) {
            //對當前段加鎖;如果成功,則進行進行後續操作;否則,調用scanAndLockForPut方法進行掃描
            HashEntry<K,V> node = tryLock() ? null :scanAndLockForPut(key, hash, value);
            V oldValue;
            try {
                HashEntry<K,V>[] tab = table;
                int index = (tab.length - 1) & hash;
                HashEntry<K,V> first = entryAt(tab, index);
                for (HashEntry<K,V> e = first;;) {
                    if (e != null) {
                        K k;
                        //判斷節點是否key和hash相等,如果是,則進行value替換
                        if ((k = e.key) == key || (e.hash == hash && key.equals(k))) {
                            oldValue = e.value;
                            if (!onlyIfAbsent) {
                                e.value = value;
                                ++modCount;
                            }
                            break;
                        }
                        e = e.next;
                    }
                    else {
                        //如果不存在該Key的映射,則在鏈表頭部插入一個新節點
                        if (node != null)
                            node.setNext(first);
                        else
                            node = new HashEntry<K,V>(hash, key, value, first);
                        int c = count + 1;
                        //如果達到閾值,則擴容
                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                            rehash(node);
                        else
                            //將新節點放入tab對於的槽位
                            setEntryAt(tab, index, node);
                        ++modCount;
                        count = c;
                        oldValue = null;
                        break;
                    }
                }
            } finally {
                unlock();
            }
            return oldValue;
        }


調用scanAndLockForPut方法,進行自旋

        /**
         * 當試圖獲取鎖時,掃描包含給定鍵的節點,如果返回,創建並返回一個鍵,確保鎖被持有。
         *  與大多數方法不同,對equals方法的調用不會被篩選:由於遍歷速度無關緊要,我們還可以幫助預熱相關的代碼和訪問。
         *
         * @return a new node if key not found, else null
         */
        private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
            //獲取hash對應槽位的第一個節點
            HashEntry<K,V> first = entryForHash(this, hash);
            HashEntry<K,V> e = first;
            HashEntry<K,V> node = null;
            int retries = -1; // 定位節點時爲負
            //嘗試獲取鎖
            while (!tryLock()) {
                //獲取失敗
                HashEntry<K,V> f; // to recheck first below
                if (retries < 0) {
                    //如果該槽位上的第一個節點爲空,則新建一個節點
                    if (e == null) {
                        if (node == null) // speculatively create node
                            node = new HashEntry<K,V>(hash, key, value, null);
                        retries = 0;
                    }
                    //如果該槽位上的第一個節點非空,且key和當前put的相同
                    else if (key.equals(e.key))
                        retries = 0;
                    //否則,繼續遍歷
                    else
                        e = e.next;
                }
                //如果達到嘗試獲取鎖的次數達到最大允許嘗試次數,則直接加鎖
                else if (++retries > MAX_SCAN_RETRIES) {
                    lock();
                    break;
                }
                else if ((retries & 1) == 0 &&(f = entryForHash(this, hash)) != first) {
                    e = first = f; // re-traverse if entry changed
                    retries = -1;
                }
            }
            return node;
        }

(10)、remove方法

    /**
     * 從映射中移除該鍵(及其對應的值)。 如果鍵不在映射中,此方法將不執行任何操作。
     *
     * @param  key the key that needs to be removed
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>
     * @throws NullPointerException if the specified key is null
     */
    public V remove(Object key) {
        int hash = hash(key);
        //通過hash定位到segment
        Segment<K,V> s = segmentForHash(hash);
        return s == null ? null : s.remove(key, hash, null);
    }

調用segment.remove方法

        /**
         * 刪除; 僅當value爲null時匹配key,否則兩者都匹配。
         */
        final V remove(Object key, int hash, Object value) {
            //嘗試加鎖
            if (!tryLock())
                scanAndLock(key, hash);
            V oldValue = null;
            try {
                HashEntry<K,V>[] tab = table;
                int index = (tab.length - 1) & hash;
                HashEntry<K,V> e = entryAt(tab, index);
                HashEntry<K,V> pred = null;
                while (e != null) {
                    K k;
                    HashEntry<K,V> next = e.next;
                    if ((k = e.key) == key ||
                        (e.hash == hash && key.equals(k))) {
                        V v = e.value;
                        if (value == null || value == v || value.equals(v)) {
                            if (pred == null)
                                setEntryAt(tab, index, next);
                            else
                                pred.setNext(next);
                            ++modCount;
                            --count;
                            oldValue = v;
                        }
                        break;
                    }
                    pred = e;
                    e = next;
                }
            } finally {
                unlock();
            }
            return oldValue;
        }
        /**
         * 當試圖獲取刪除或替換操作的鎖時,掃描包含給定鍵的節點。 返回時,保證鎖被持有。
         * 注意,即使沒有找到鍵,我們也必須鎖定,以確保更新的順序一致性。
         */
        private void scanAndLock(Object key, int hash) {
            // similar to but simpler than scanAndLockForPut
            HashEntry<K,V> first = entryForHash(this, hash);
            HashEntry<K,V> e = first;
            int retries = -1;
            while (!tryLock()) {
                HashEntry<K,V> f;
                if (retries < 0) {
                    if (e == null || key.equals(e.key))
                        retries = 0;
                    else
                        e = e.next;
                }
                else if (++retries > MAX_SCAN_RETRIES) {
                    lock();
                    break;
                }
                else if ((retries & 1) == 0 &&
                         (f = entryForHash(this, hash)) != first) {
                    e = first = f;
                    retries = -1;
                }
            }
        }

數據結構

總結:

(1)、ConcurrentHashMap1.7底層數據結構主要是分段鎖+數組+鏈表; 分段鎖Segment通過繼承ReentrantLock來實現;

(2)、進行put、remove等操作時,它會將hash值對於的段進行加鎖,然後進行相應的操作。加鎖的時候,剛開始會實行嘗試獲取自旋,然後超過最大嘗試次數後,直接加鎖。

(3)、另外,底層實現上使用volatile,sun.misc.Unsafe的getObjectVolatile()、putOrderedObject()、getObject()等等一些底層方法來實現。關於Unsafe這一塊的知識,後期再進行補充!

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