ConcurrentHashMap 源碼淺析 1.7

  • 簡介

    (1) 背景
    HashMap死循環:HashMap在併發執行put操作時會引起死循環,是因爲多線程會導致HashMap的Entry鏈表形成環形數據結構,一旦形成環形數據結構,Entry的next節點永遠不爲空,就會產生死循環獲取Entry.
    HashTable效率低下:HashTable容器使用synchronized來保證線程安全,但在線程競爭激烈的情況下HashTable的效率非常低下.因爲當一個線程訪問HashTable的同步方法,其它線程也訪問HashTable的同步方法時,會進入阻塞或輪詢狀態.如線程1使用put進行元素添加,線程2不但不能使用put方法添加元素,也不能使用get方法獲取元素,所以競爭越激烈效率越低.
    (2) 簡介
    HashTable容器在競爭激烈的併發環境下表現出效率低下的原因是所有訪問HashTable的線程都必須競爭一把鎖,假如容器裏有多把鎖,每一把鎖用於鎖容器其中一部分數據,那麼多線程訪問容器裏不同的數據段時,線程間不會存在競爭,從而可以有效提高併發訪問效率,這就是ConcurrentHash所使用的鎖分段技術.首先將數據分成一段一段地儲存,然後給每一段配一把鎖,當一個線程佔用鎖訪問其中一段數據時,其它段的數據也能被其它線程訪問.

  • 結構

    ConcurrentHash是由Segments數組結構和HashEntry數組結構組成.Segment是一種可重入鎖(ReentrantLock),在ConcurrentHashMap裏扮演鎖的色;HashEntry則用於存儲鍵值對數據.一個ConcurrentHashMap裏包含一個Segment組.Segment的結構和HashMap類似,是一種數組加鏈表的結構.一個Segment裏包含一個HashEntry數組,每個HashEntry是一個鏈表結構的元素,每個Segment守護者一個HashEntry數組裏面的元素,當對HashEntry數組的數據進行修改時,必須先獲得與它對應的Segment鎖,如下圖所示.

    ConcurrentHashMap 源碼淺析 1.7

  • 基本成員
    default_initial_capacitymap默認容量,必須是2的冥

    /**
     * 默認的初始容量 16
     */
    static final int DEFAULT_INITIAL_CAPACITY = 16;

    default_load_factor默認負載因子(存儲的比例)

    /**
     * 默認的負載因子
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    default_concurrency_level默認併發數量,segments數組量(ps:初始化後不能修改)

    /**
     * 默認的併發數量,會影響segments數組的長度(初始化後不能修改)
     */
    static final int DEFAULT_CONCURRENCY_LEVEL = 16;

    maximum_capacitymap最大容量

    /**
     * 最大容量,構造ConcurrentHashMap時指定的值超過,就用該值替換
     * ConcurrentHashMap大小必須是2^n,且小於等於2^30
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    min_segment_table_capacityHashEntry[]默認容量

    /**
     * 每個segment中table數組的長度,必須是2^n,至少爲2
     */
    static final int MIN_SEGMENT_TABLE_CAPACITY = 2;

    max_segments最大併發數,segments數組最大量

    /**
     * 允許最大segment數量,用於限定concurrencyLevel的邊界,必須是2^n
     */
    static final int MAX_SEGMENTS = 1 << 16;

    retries_before_lock重試次數,在加鎖之前

    /**
     * 非鎖定情況下調用size和contains方法的重試次數,避免由於table連續被修改導致無限重試
     */
    static final int RETRIES_BEFORE_LOCK = 2;

    segmentMask計算segment位置的掩碼(segments.length-1)

    /**
     * 用於segment的掩碼值,用於與hash的高位進行取&
     */
    final int segmentMask;

    segmentShift

    /**
     * 用於算segment位置時,hash參與運算的位數
     */
    final int segmentShift;

    segmentssegment數組

    /**
     * segments數組
     */
    final Segment<K,V>[] segments;

    HashEntry存儲數據的鏈式結構

    static final class HashEntry<K,V> {
        // hash值
        final int hash;
        // key
        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語義寫入next,保證可見性
         */
        final void setNext(HashEntry<K,V> n) {
            UNSAFE.putOrderedObject(this, nextOffset, n);
        }

    Segment繼承ReentrantLock鎖,用於存放HashEntry[]

    static final class Segment<K,V> extends ReentrantLock implements Serializable {
        private static final long serialVersionUID = 2249069246763182397L;
    
        /**
         * 對segment加鎖時,在阻塞之前自旋的次數
         *
         */
        static final int MAX_SCAN_RETRIES =
                Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
    
        /**
         * 每個segment的HashEntry table數組,訪問數組元素可以通過entryAt/setEntryAt提供的volatile語義來完成
         * volatile保證可見性
         */
        transient volatile HashEntry<K,V>[] table;
    
        /**
         * 元素的數量,只能在鎖中或者其他保證volatile可見性之間進行訪問
         */
        transient int count;
    
        /**
         * 當前segment中可變操作發生的次數,put,remove等,可能會溢出32位
         * 它爲chm isEmpty() 和size()方法中的穩定性檢查提供了足夠的準確性.
         * 只能在鎖中或其他volatile讀保證可見性之間進行訪問
         */
        transient int modCount;
    
        /**
         * 當table大小超過閾值時,對table進行擴容,值爲(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;
        }
  • 構造方法
    有參構造
    /**
     * ConcurrentHashMap 構造方法
     * @param initialCapacity 初始化容量
     * @param loadFactor 負載因子
     * @param concurrencyLevel 併發segment,segments數組的長度
     */
    public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
        if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
        // 大於最大segments容量,取最大容量
        if (concurrencyLevel > MAX_SEGMENTS)
            concurrencyLevel = MAX_SEGMENTS;
        // Find power-of-two sizes best matching arguments
        // 2^sshift = ssize 例如:sshift = 4,ssize = 16
        // 根據concurrencyLevel計算出ssize爲segments數組的長度
        int sshift = 0;
        int ssize = 1;
        while (ssize < concurrencyLevel) { // 第一次 滿足
            ++sshift;  // 第一次 1
            ssize <<= 1; // 第一次 ssize = ssize << 1 (1 * 2^1)
        }
        // segmentShift和segmentMask的定義
        this.segmentShift = 32 - sshift; // 用於計算hash參與運算位數
        this.segmentMask = ssize - 1; // segments位置範圍
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        // 計算每個segment中table的容量
        int c = initialCapacity / ssize;
        if (c * ssize < initialCapacity)
            ++c;
        // HashEntry[]默認 容量
        int cap = MIN_SEGMENT_TABLE_CAPACITY;
        // 確保cap是2^n
        while (cap < c)
            cap <<= 1;
        // create segments and segments[0]
        // 創建segments並初始化第一個segment數組,其餘的segment延遲初始化
        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;
    }

    無參構造使用默認參數
    public ConcurrentHashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
    }

  • 基本方法


    一些UNSAFE方法
    HashEntry
    setNext

    /**
         * 使用volatile語義寫入next,保證可見性
         */
        final void setNext(HashEntry<K,V> n) {
            UNSAFE.putOrderedObject(this, nextOffset, n);
        }

    entryAt get HashEntry

    /**
     * 獲取給定table的第i個元素,使用volatile讀語義
     */
    static final <K,V> HashEntry<K,V> entryAt(HashEntry<K,V>[] tab, int i) {
        return (tab == null) ? null :
                (HashEntry<K,V>) UNSAFE.getObjectVolatile
                        (tab, ((long)i << TSHIFT) + TBASE);
    }

    setEntryAt set HashEntry

    /**
     * 設置給定的table的第i個元素,使用volatile寫語義
     */
    static final <K,V> void setEntryAt(HashEntry<K,V>[] tab, int i,
                                       HashEntry<K,V> e) {
        UNSAFE.putOrderedObject(tab, ((long)i << TSHIFT) + TBASE, e);
    }

    put 插入元素
    執行流程分析
    (1) map的put方法就做了三件事情,找出segments的位置;判斷當前位置有沒有初始化,沒有就調用ensureSegment()方法初始化;然後調用segment的put方法.
    (2) segment的put方法.,獲取當前segment的鎖,成功接着執行,失敗調用scanAndLockForPut方法自旋獲取鎖,成功後也是接着往下執行.
    (3) 通過hash計算出位置,獲取節點,找出相同的key和hash替換value,返回.沒有找到相同的,設置找出的節點爲當前創建節點的next節點,設置創建節點前,判斷是否需要擴容,需要調用擴容方法rehash();不需要,設置節點,返回,釋放鎖.

    /**
     * map的put方法,定位segment
     */
    public V put(K key, V value) {
        Segment<K,V> s;
        // value不能爲空
        if (value == null)
            throw new NullPointerException();
        // 獲取hash
        int hash = hash(key);
        // 定位segments 數組的位置
        int j = (hash >>> segmentShift) & segmentMask;
        // 獲取這個segment
        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
                (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
            // 爲null 初始化當前位置的segment
            s = ensureSegment(j);
        return s.put(key, hash, value, false);
    }
        /**
         * put到table方法
         */
        final V put(K key, int hash, V value, boolean onlyIfAbsent) {
            // 是否獲取鎖,失敗自旋獲取鎖(直到成功)
            HashEntry<K,V> node = tryLock() ? null :
                    scanAndLockForPut(key, hash, value);
            V oldValue;
            try {
                HashEntry<K,V>[] tab = table;
                // 定義位置
                int index = (tab.length - 1) & hash;
                // 獲取第一個桶的第一個元素
                // entryAt 底層調用getObjectVolatile 具有volatile讀語義
                HashEntry<K,V> first = entryAt(tab, index);
                for (HashEntry<K,V> e = first;;) {
                    if (e != null) { // 證明鏈式結構有數據 遍歷節點數據替換,直到e=null
                        K k;
                        if ((k = e.key) == key ||
                                (e.hash == hash && key.equals(k))) { //  找到了相同的key
                            oldValue = e.value;
                            if (!onlyIfAbsent) { // 默認值false
                                e.value = value; // 替換value
                                ++modCount;
                            }
                            break; // 結束循環
                        }
                        e = e.next;
                    }
                    else { // e=null (1) 之前沒有數據 (2) 沒有找到替換的元素
                        // node是否爲空,這個獲取鎖的是有關係的
                        // (1) node不爲null,設置node的next爲first
                        // (2) node爲null,創建頭節點,指定next爲first
                        if (node != null)
                            // 底層使用 putOrderedObject 方法 具有volatile寫語義
                            node.setNext(first);
                        else
                            node = new HashEntry<K,V>(hash, key, value, first);
                        int c = count + 1;
                        // 擴容條件 (1)entry數量大於閾值 (2) 當前table的數量小於最大容量  滿足以上條件就擴容
                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                            // 擴容方法,方法裏面具體講
                            rehash(node);
                        else
                            // 給table的index位置設置爲node,
                            // node爲頭結點,原來的頭結點first爲node的next節點
                            // 底層也是調用的 putOrderedObject 方法 具有volatile寫語義
                            setEntryAt(tab, index, node);
                        ++modCount;
                        count = c;
                        oldValue = null;
                        break;
                    }
                }
            } finally {
                unlock();
            }
            return oldValue;
        }

    解釋下(hash >>> segmentShift) & segmentMask定位segment位置(個人理解)
    ConcurrentHashMap 源碼淺析 1.7
    ensureSegment初始化segment方法
    執行流程
    (1) 計算位置,使用UNSAFE的方法判斷當前位置有沒有初始化,然後使用segmets[0]的模板創建一個新的HashEntry[],再次判斷當前位置有沒有初始化,可能存在多線程同時初始化,然後創建一個新的segment,最後使用自旋cas設置新的segment的位置,保證只有一個線程初始化成功.

    /**
     *
     * @param k 位置
     * @return segments
     */
    private Segment<K,V> ensureSegment(int k) {
        final Segment<K,V>[] ss = this.segments;  // 當前的segments數組
        long u = (k << SSHIFT) + SBASE; // raw offset // 計算原始偏移量,在segments數組的位置
        Segment<K,V> seg;
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) { // 判斷沒有被初始化
            Segment<K,V> proto = ss[0]; // use segment 0 as prototype // 獲取第一個segment ss[0]
            // 這就是爲什麼要在初始化化map時要初始化一個segment,需要用cap和loadFactoe 爲模板
            int cap = proto.table.length; // 容量
            float lf = proto.loadFactor; // 負載因子
            int threshold = (int)(cap * lf); // 閾值
            // 初始化ss[k] 內部的tab數組 // recheck
            HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
            // 再次檢查這個ss[k]  有沒有被初始化
            if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                    == null) { // recheck
                // 創建一個Segment
                Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
                // 這裏用自旋CAS來保證把segments數組的u位置設置爲s
                // 萬一有多線程執行到這一步,只有一個成功,break
                // getObjectVolatile 保證了讀的可見性,所以一旦有一個線程初始化了,那麼就結束自旋
                while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                        == null) {
                    if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                        break;
                }
            }
        }
        return seg;
    }

    scanAndLockForPut自旋獲取鎖方法
    具體流程看代碼註釋,解釋下這個方法的優化(看的某位大佬的博客)
    (1) 我們在put方法獲取鎖失敗,纔會進入這個方法,這個方法採用自旋獲取鎖,直到成功才返回,但是使用了自旋次數的限制,這麼做的好處是什麼了,就是競爭太激烈的話,這個線程可能一直獲取不到鎖,自旋也是消耗cpu性能的,所以當達到自旋次數時,就阻塞當前線程,直到有線程釋放了鎖,通知這些線程.在等待過程中是不消耗cpu的.
    (2) 當我們進入這個方法時,說明獲取鎖失敗,那麼可別是別的線程在對這個segment進行修改操作,所以說如果別的線程在操作之後,我們自己的工作內存中的數據可能已經不是最新的了,這個時候我們使用具有volatile語義的方法重新讀了數據,在自旋過程中遍歷這些數據,把最新的數據緩存在工作內存中,當前線程再次獲取鎖時,我們的數據是最新的,就不用重新去住內存中獲取,這樣在自旋獲取的鎖的過程中就預熱了這些數據,在獲取鎖之後的執行中就提升了效率.

    private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
            HashEntry<K,V> first = entryForHash(this, hash); // 根據hash獲取頭結點
            HashEntry<K,V> e = first;
            HashEntry<K,V> node = null;
            int retries = -1; // 是爲了找到對應hash桶,遍歷鏈表時找到就停止
            while (!tryLock()) { // 嘗試獲取鎖,成功就返回,失敗就開始自旋
                HashEntry<K,V> f; // to recheck first below
                if (retries < 0) {
                    if (e == null) {  // 結束遍歷節點
                        if (node == null) // 創造新的節點
                            node = new HashEntry<K,V>(hash, key, value, null);
                        retries = 0; // 結束遍歷
                    }
                    else if (key.equals(e.key)) // 找到節點 停止遍歷
                        retries = 0;
                    else
                        e = e.next; // 下一個節點 直到爲null
                }
                else if (++retries > MAX_SCAN_RETRIES) { // 達到自旋的最大次數
                    lock(); // 進入加鎖方法,失敗進入隊列,阻塞當前線程
                    break;
                }
                else if ((retries & 1) == 0 &&
                        (f = entryForHash(this, hash)) != first) {
                    e = first = f; // 頭結點變化,需要重新遍歷,說明有新的節點加入或者移除
                    retries = -1;
                }
            }
            return node;
        }

    rehash擴容方法
    解釋下節點位置變化這一塊的處理,如下圖所示.
    ConcurrentHashMap 源碼淺析 1.7

    /**
         *擴容方法
         */
        private void rehash(HashEntry<K,V> node) {
    
            // 舊的table
            HashEntry<K,V>[] oldTable = table;
            // 舊的table的長度
            int oldCapacity = oldTable.length;
            // 擴容原來capacity的一倍
            int newCapacity = oldCapacity << 1;
            // 新的閾值
            threshold = (int)(newCapacity * loadFactor);
            // 新的table
            HashEntry<K,V>[] newTable =
                    (HashEntry<K,V>[]) new HashEntry[newCapacity];
            // 新的掩碼
            int sizeMask = newCapacity - 1;
            // 遍歷舊的table
            for (int i = 0; i < oldCapacity ; i++) {
                // table中的每一個鏈表元素
                HashEntry<K,V> e = oldTable[i];
                if (e != null) { // e不等於null
                    HashEntry<K,V> next = e.next; // 下一個元素
                    int idx = e.hash & sizeMask;  // 重新計算位置,計算在新的table的位置
                    if (next == null)   //  Single node on list 證明只有一個元素
                        newTable[idx] = e; // 把當前的e設置給新的table
                    else { // Reuse consecutive sequence at same slot
                        HashEntry<K,V> lastRun = e; // 當前e
                        int lastIdx = idx;          // 在新table的位置
                        for (HashEntry<K,V> last = next;
                             last != null;
                             last = last.next) { // 遍歷鏈表
                            int k = last.hash & sizeMask; // 確定在新table的位置
                            if (k != lastIdx) { // 頭結點和頭結點的next元素的節點發生了變化
                                lastIdx = k;    // 記錄變化位置
                                lastRun = last; // 記錄變化節點
                            }
                        }
                        // 以下把鏈表設置到新table分爲兩種情況
                        // (1) lastRun 和 lastIdx 沒有發生變化,也就是整個鏈表的每個元素位置和一樣,都沒有發生變化
                        // (2) lastRun 和 lastIdx 發生了變化,記錄變化位置和變化節點,然後把變化的這個節點設置到新table
                        //     ,但是整個鏈表的位置只有變化節點和它後面關聯的節點是對的
                        //      下面的這個遍歷就是處理這個問題,遍歷當前頭節點e,找出不等於變化節點(lastRun)的節點重新處理
                        newTable[lastIdx] = lastRun;
                        // Clone remaining nodes
                        for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
                            V v = p.value;
                            int h = p.hash;
                            int k = h & sizeMask;
                            HashEntry<K,V> n = newTable[k];
                            newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
                        }
                    }
                }
            }
            // 處理擴容時那個添加的節點
    
            // 計算位置
            int nodeIndex = node.hash & sizeMask; // add the new node
            // 設置next節點,此時已經擴容完成,要從新table裏面去當前位置的頭結點爲next節點
            node.setNext(newTable[nodeIndex]);
            // 設置位置
            newTable[nodeIndex] = node;
            // 新table替換舊的table
            table = newTable;
        }

    get 沒有加鎖,效率高
    注意:get方法使用了getObjectVolatile方法讀取segment和hashentry,保證是最新的,具有鎖的語義,可見性
    分析:爲什麼get不加鎖可以保證線程安全
    (1) 首先獲取value,我們要先定位到segment,使用了UNSAFE的getObjectVolatile具有讀的volatile語義,也就表示在多線程情況下,我們依舊能獲取最新的segment.
    (2) 獲取hashentry[],由於table是每個segment內部的成員變量,使用volatile修飾的,所以我們也能獲取最新的table.
    (3) 然後我們獲取具體的hashentry,也時使用了UNSAFE的getObjectVolatile具有讀的volatile語義,然後遍歷查找返回.
    (4) 總結我們發現怎個get過程中使用了大量的volatile關鍵字,其實就是保證了可見性(加鎖也可以,但是降低了性能),get只是讀取操作,所以我們只需要保證讀取的是最新的數據即可.

    /**
     * get 方法
     */
    public V get(Object key) {
        Segment<K,V> s; // manually integrate access methods to reduce overhead
        HashEntry<K,V>[] tab;
        int h = hash(key);
        long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE; // 獲取segment的位置
        // getObjectVolatile getObjectVolatile語義讀取最新的segment,獲取table
        if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
                (tab = s.table) != null) {
            // getObjectVolatile getObjectVolatile語義讀取最新的hashEntry,並遍歷
            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;
                // 找到相同的key 返回
                if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                    return e.value;
            }
        }
        return null;
    }

    size 嘗試3次不加鎖獲取sum,如果發生變化就全部加鎖,size和containsValue方法的思想也是基本類似.
    執行流程
    (1) 第一次,retries++=0,不滿足全部加鎖條件,遍歷所有的segment,sum就是所有segment的容量,last等於0,第一次不相等,last=sum.
    (2) 第二次,retries++=1,不滿足加鎖條件,計算所有的segment,sum就是所有的segment的容量,last是上一次的sum,相等結束循環,不相等下次循環.
    (3) 第三次,retries++=2,先運算後賦值,所以此時還是不滿足加鎖條件和上面一樣統計sum,判斷這一次的sum和last(上一次的sum)是否相等,相等結束,不相等,下一次循環.
    (4) 第四次,retries++=2,滿足加鎖條件,給segment全部加鎖,這樣所有線程就沒有辦法進行修改操作,統計每個segment的數量求和,然後返回size.(ps:全部加鎖提高了size的準確率,但是降低了吞吐量,統計size的過程中如果其它線程進行修改操作這些線程全部自旋或者阻塞).

    /**
     * size
     * @return
     */
    public int size() {
        // Try a few times to get accurate count. On failure due to
        // continuous async changes in table, resort to locking.
        final Segment<K,V>[] segments = this.segments;
        int size;
        boolean overflow; // 爲true表示size溢出32位
        long sum;         // modCounts的總和
        long last = 0L;   // previous sum
        int retries = -1; // 第一次不計算次數,所以會重試三次
        try {
            for (;;) {
                if (retries++ == RETRIES_BEFORE_LOCK) { // 重試次數達到3次 對所有segment加鎖
                    for (int j = 0; j < segments.length; ++j)
                        ensureSegment(j).lock(); // force creation
                }
                sum = 0L;
                size = 0;
                overflow = false;
                for (int j = 0; j < segments.length; ++j) {
                    Segment<K,V> seg = segmentAt(segments, j);
                    if (seg != null) { // seg不等於空
                        sum += seg.modCount; // 不變化和size一樣
                        int c = seg.count; // seg 的size
                        if (c < 0 || (size += c) < 0)
                            overflow = true;
                    }
                }
                if (sum == last) // 沒有變化
                    break;
                last = sum; // 變化,記錄這一次的變化值,下次循環時對比.
            }
        } finally {
            if (retries > RETRIES_BEFORE_LOCK) {
                for (int j = 0; j < segments.length; ++j)
                    segmentAt(segments, j).unlock();
            }
        }
        return overflow ? Integer.MAX_VALUE : size;
    }

    remove replace和remove都是用了scanAndLock這個方法
    解釋下scanAndLock這個方法,和put方法的scanAndLockForPut方法思想類似,都採用了同樣的優化手段.

    /**
         * 刪除方法
         */
        final V remove(Object key, int hash, Object value) {
            if (!tryLock()) // 獲取鎖
                scanAndLock(key, hash); // 自旋獲取鎖
            V oldValue = null;
            try {
                HashEntry<K,V>[] tab = table; // 當前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))) { // e的key=傳入的key
                        V v = e.value; // 獲取value
                        if (value == null || value == v || value.equals(v)) { // 如果value相等
                            if (pred == null) // 說明是頭結點,讓next節點爲頭結點即可
                                setEntryAt(tab, index, next);
                            else //  說明不是頭結點,就把當前節點e的上一個節點pred的next節點設置爲當前e的next節點
                                pred.setNext(next);
                            ++modCount;
                            --count;
                            oldValue = v;
                        }
                        break;
                    }
                    pred = e;
                    e = next;
                }
            } finally {
                unlock();
            }
            return oldValue;
        }

    isEmpty 其實也和size的思想類似,不過這個始終沒有加鎖,提高了性能
    執行流程
    (1) 第一次遍歷就幹了一件事,確定map的每個segment是否爲0,其中任何一個segment的count不爲0,就返回,都爲0,就累加modCount爲sum.
    (2) 判斷sum是否爲0,不爲0,就代表了map之前是有數據的,被remove和clean了,modCount指的是操作次數,再次確定map的每個segment是否爲0,其中任何一個segment的count不爲0,就返回,並且累減sum,最後判斷sum是否爲0,爲0就代表沒有任何變化,不爲0,就代表在第一次統計過程中有線程又添加了元素,所以返回false.但是如果在第二次統計時又發生了變化了,所以這個不是那麼的準確,但是不加鎖提高了性能,也是一個可以接受的方案.
    ```public boolean isEmpty() {

    long sum = 0L;
    final Segment<K,V>[] segments = this.segments;
    for (int j = 0; j < segments.length; ++j) {
        Segment<K,V> seg = segmentAt(segments, j);
        if (seg != null) {
            if (seg.count != 0)
                return false; // 某一個不爲null,立即返回
            sum += seg.modCount;
        }
    }
    // 上面執行完 說明不爲空,並且過程可能發生了變化
    // 發生變化
    if (sum != 0L) { // recheck unless no modifications
        for (int j = 0; j < segments.length; ++j) {
            Segment<K,V> seg = segmentAt(segments, j);
            if (seg != null) {
                if (seg.count != 0)
                    return false;
                sum -= seg.modCount;
            }
        }
        if (sum != 0L) // 變化值沒有爲0,說明不爲空
            return false;
    }
    
    // 沒有發生變化
    return true;

    }

  • 總結

    1.7 ConcurrentHashMap 使用了分段鎖的思想提高了併發的的訪問量,就是使用很多把鎖,每一個segment代表了一把鎖,每一段只能有一個線程獲取鎖;但是segment的數量初始化了,就不能修改,所以這也代表了併發的不能修改,這也是1.7的一個侷限性.從get方法可以看出使用了UNSAFE的一些方法和volatile關鍵字來代替鎖,提高了併發性.在size和containsValue這些方法提供一種嘗試思想,先不加鎖嘗試統計,如果其中沒有變化就返回,有變化接着嘗試,達到嘗試次數再加鎖,這樣也避免了立即加鎖對併發的影響.

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