源碼分析ConcurrentHashMap(jdk1.7 和jdk1.8)

JDK1.7 ConcurrentHashMap


圖示結構

圖片來源於https://www.javadoop.com/post/hashmap圖片來源於https://www.javadoop.com/post/hashmap]


初始化

ConcurrentHashMap的初始化是個懶加載的過程:實例化ConcurrentHashMap時只新建了Segment數組並初始化了s[0],真正對Segment其他位置的初始化要等到put操作才執行。
initialCapacity:初始容量,這個值時整個ConcurrentHashMap的初始容量,實際操作時要平均分給每個Segment。
loadFactor:負載因子,1.7版本的ConcurrentHashMap和1.7版本的HashMap不同的地方在於Segment數組不可以擴容,所以這個負載因子是給每個Segment內部使用的。

  @SuppressWarnings("unchecked")
    public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
        if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
        //併發數最大爲2^16
        if (concurrencyLevel > MAX_SEGMENTS)
            concurrencyLevel = MAX_SEGMENTS;
        // Find power-of-two sizes best matching arguments
        int sshift = 0;
        int ssize = 1;
        //ssize爲並行級別,保證其爲2的n次方
        while (ssize < concurrencyLevel) {
            ++sshift;
            ssize <<= 1;
        }
        //默認情況下concurrencyLevel爲16 ,所以sshift爲4 ,ssize爲16,掩碼segmentMask爲15,移位數segmentShift爲28
        this.segmentShift = 32 - sshift;
        this.segmentMask = ssize - 1;
        //數組最大長度2^30
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        //c默認爲1,用於計算每個segment的數組長度。每個segment的長度爲不小於c的2的最小次冪
        int c = initialCapacity / ssize;
        if (c * ssize < initialCapacity)
            ++c;

        //每個segment的數組長度,默認值爲2,這樣擴容閾值爲2*0.75=1.5,像segment中插入第一個元素不會擴容,插入第二個則進行擴容
        int cap = MIN_SEGMENT_TABLE_CAPACITY;
        while (cap < c)
            cap <<= 1;
        //創建 segments和 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];
        //往數組中寫入s0,其他位置還是null
        UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
        this.segments = ss;
    }

初始化完成後,我們得到一個Segment數組,其中的segment[0]也被初始化了。


put過程

    @SuppressWarnings("unchecked")
    public V put(K key, V value) {
        Segment<K,V> s;
        //1. 首先校驗value不能爲空
        if (value == null)
            throw new NullPointerException();
        //2. 計算key的hash值
        int hash = hash(key);
        //3. 根據hash值找到Segment數組中的位置j,  j爲hash的高四位
        int j = (hash >>> segmentShift) & segmentMask;
        //4. 如果ensureSegment(j)爲空,則對 segment[j] 進行初始化
        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
            s = ensureSegment(j);//初始化segment[j]
        //5. 將新值插入到槽s[j]中
        return s.put(key, hash, value, false);//進入segment的put方法
    }

	/**
	 * 作用:初始化segment[k]
	 * 過程:循環使用CAS操作進行併發控制
	 */
  	@SuppressWarnings("unchecked")
    private Segment<K,V> ensureSegment(int k) {
        final Segment<K,V>[] ss = this.segments;
        long u = (k << SSHIFT) + SBASE; // raw offset
        Segment<K,V> seg;
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
            //使用ss[0]作爲原型
            Segment<K,V> proto = ss[0]; // use segment 0 as prototype
            int cap = proto.table.length;
            float lf = proto.loadFactor;
            int threshold = (int)(cap * lf);
            //初始化segment[k]內部數組
            HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
            //再次檢查該槽是否爲空
            if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                    == null) { // recheck
                Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
                //使用循環CAS 初始化
                while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                        == null) {
                    if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                        break;
                }
            }
        }
        return seg;
    }

	/**
	 * segment的put方法
	 * 方法第一步:獲取獨佔鎖
	 */
     final V put(K key, int hash, V value, boolean onlyIfAbsent) {
            //在往segment中寫入前要先獲得該segment的獨佔鎖
            HashEntry<K,V> node = tryLock() ? null :
                scanAndLockForPut(key, hash, value);
            V oldValue;
            try {
                //segment內部數組
                HashEntry<K,V>[] tab = table;
                //再利用hash值求內部數組對應存放位置下標
                int index = (tab.length - 1) & hash;
                //first爲數組該位置處的鏈表表頭
                HashEntry<K,V> first = entryAt(tab, index);
                for (HashEntry<K,V> e = first;;) {
                    if (e != null) {
                        K k;
                        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 {
                        if (node != null)
                            node.setNext(first);
                        else
                            //沒有找到相同的key,則將新結點插入到鏈表表頭
                            node = new HashEntry<K,V>(hash, key, value, first);
                        int c = count + 1;
                        //如果超過了segment閾值,則需要擴容
                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                            rehash(node);
                        else
                            setEntryAt(tab, index, node);
                        ++modCount;
                        count = c;
                        oldValue = null;
                        break;
                    }
                }
            } finally {
                //解鎖
                unlock();
            }
            return oldValue;
        }

		/**
		 * tryLock獲取鎖失敗,則通過該方法獲得鎖。
		 * 該方法有兩個出口:1. tryLock方法成功,循環結束   2. 重試次數大於MAX_SCAN_RETRIES,進入lock方法,此方法會阻塞等待,知道成功拿到獨佔鎖,然後break中斷循環
		 * 這個方法就是看似複雜,但是其實就是做了一件事,那就是獲取該 segment 的獨佔鎖,如果需要的話順便實例化了一下 node。
		 */
       private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
            HashEntry<K,V> first = entryForHash(this, hash);
            HashEntry<K,V> e = first;
            HashEntry<K,V> node = null;
            int retries = -1; // negative while locating node
            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;
                    }
                    else if (key.equals(e.key))
                        retries = 0;
                    else
                        e = e.next;
                }
                //重試次數大於MAX_SCAN_RETRIES:單核1,多核64,則不搶了,進入阻塞隊列等待
                //lock爲阻塞方法,直到獲取鎖後返回
                else if (++retries > MAX_SCAN_RETRIES) {
                    lock();
                    break;
                }
                else if ((retries & 1) == 0 &&
                        //當有新的元素進來成爲表頭,就需要重新走一遍scanAndLockForPut方法
                         (f = entryForHash(this, hash)) != first) {
                    e = first = f; // re-traverse if entry changed
                    retries = -1;
                }
            }
            return node;
        }

rehash過程

Segment數組不能擴容,擴容指的是Segment數組中具體某個位置的小數組 HashEntry<K,V>[] 進行擴容,擴容後大小爲原來的2倍。

       /**
         * 對具體segment下的table進行擴容並進行重哈希,然後將新的結點node加入其中
         */
        @SuppressWarnings("unchecked")
        private void rehash(HashEntry<K, V> node) {
            HashEntry<K, V>[] oldTable = table;
            int oldCapacity = oldTable.length;
            //2倍擴容
            int newCapacity = oldCapacity << 1;
            //新的閾值
            threshold = (int) (newCapacity * loadFactor);
            //新的table
            HashEntry<K, V>[] newTable =
                    (HashEntry<K, V>[]) new HashEntry[newCapacity];
            int sizeMask = newCapacity - 1;
            for (int i = 0; i < oldCapacity; i++) {
                //e是鏈表中的第一個元素
                HashEntry<K, V> e = oldTable[i];
                if (e != null) {
                    HashEntry<K, V> next = e.next;
                    int idx = e.hash & sizeMask;
                    //1. 鏈表只有一個元素,則直接將結點轉移到新table中
                    if (next == null)   //  Single node on list
                        newTable[idx] = e;
                    //2. 鏈表中不止一個元素
                    else { // Reuse consecutive sequence at same slot
                        HashEntry<K, V> lastRun = e;
                        int lastIdx = idx;
                        //2.1 通過for循環會找到一個lastRun結點,該結點以及以後的結點 重新定位的新索引都相同,即該結點之後的所有元素都要放到一起
                        for (HashEntry<K, V> last = next; last != null;last = last.next) {
                            int k = last.hash & sizeMask;
                            if (k != lastIdx) {
                                lastIdx = k;
                                lastRun = last;
                            }
                        }
                        //2.2 放置lastRun及其之後的所有結點
                        newTable[lastIdx] = lastRun;
                        //2.3 放置鏈表頭結點e和lastRun之間的結點
                        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);
                        }
                    }
                }
            }
            //3. 所有的元素重哈希後將新來的node放在新的table中相應的鏈表頭部
            int nodeIndex = node.hash & sizeMask; // add the new node
            node.setNext(newTable[nodeIndex]);
            newTable[nodeIndex] = node;
            table = newTable;
        }

get過程

    public V get(Object key) {
        Segment<K, V> s; // manually integrate access methods to reduce overhead
        HashEntry<K, V>[] tab;
        //1. 獲得hash值
        int h = hash(key);
        long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
        //2. 根據hash值找到對應的segment
        if ((s = (Segment<K, V>) UNSAFE.getObjectVolatile(segments, u)) != null &&
                (tab = s.table) != null) {
                //3. 找到segment 內部數組相應位置的鏈表,遍歷
            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;
    }

remove過程

    public V remove(Object key) {
        int hash = hash(key);
        Segment<K, V> s = segmentForHash(hash);
        return s == null ? null : s.remove(key, hash, null);
    }

	    /**
         * segment的remove方法
         */
       final V remove(Object key, int hash, Object value) {
       		//1. 上鎖
            if (!tryLock())
                scanAndLock(key, hash);
            V oldValue = null;
            try {
                HashEntry<K, V>[] tab = table;
                //2. 由hash值獲得具體的數組下標index
                int index = (tab.length - 1) & hash;
                //3.獲得對應鏈表的頭部結點
                HashEntry<K, V> e = entryAt(tab, index);
                //4. 使用前驅結點指針
                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;
        }

併發問題分析

由上面的分析可知,put和remove操作都需要給segment加上獨佔鎖,rehash過程是在put操作過程中進行的,也屬於獲取了獨佔鎖,因此無需考慮它們線程安全問題,然而get操作是不加鎖的,因此需要考慮的問題:get的時候在同一個segment中發生了put或者remove操作。

  1. put操作的線程安全性
    1) 初始化槽。在ensureSegment方法中進行,通過循環CAS操作實現,保證了線程安全性。
    2) 添加結點到鏈表頭部。若get操作在鏈表遍歷的過程中已經到了鏈表中間,則無影響;若get操作發生在put操作之後,需要保證剛插入鏈表頭部的新結點被讀取,這主要靠 setEntryAt 方法中使用的 UNSAFE.putOrderedObject 來實現。

    /**
     * Gets the ith element of given table (if nonnull) with volatile
     * read semantics 讀語義. Note: This is manually integrated into a few
     * performance-sensitive methods to reduce call overhead.
     */
    @SuppressWarnings("unchecked")
    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);
    }
    
    /**
     * Sets the ith element of given table, with volatile write
     * semantics 寫語義. (See above about use of putOrderedObject.)
     */
    static final <K, V> void setEntryAt(HashEntry<K, V>[] tab, int i,
                                        HashEntry<K, V> e) {
        UNSAFE.putOrderedObject(tab, ((long) i << TSHIFT) + TBASE, e);
    }
    

    3)rehash。 擴容的過程是新建table數組,然後將舊table元素進行遷移,完成後將table指針指向新table。 若get發生在rehash之前,那麼就在舊table上做查詢操作;若put發生在rehash之前,那麼put操作的可見性保證就是table使用了volatile關鍵字。

  2. remove操作的線程安全性
    1)如果remove破壞的結點get操作已經遍歷過去了,則沒有問題;
    2)如果remove先行破壞了結點:1、如果此節點是頭結點,那麼需要將頭結點的 next 設置爲數組該位置的元素,table 雖然使用了 volatile 修飾,但是 volatile 並不能提供數組內部操作的可見性保證,所以源碼中使用了 UNSAFE 來操作數組,請看方法 setEntryAt;2、如果要刪除的節點不是頭結點,它會將要刪除節點的後繼節點接到前驅節點中,這裏的併發保證就是 next 屬性是 volatile 的。

        /**
         * Sets next field with volatile write semantics寫語義.  (See above
         * about use of putOrderedObject.)
         */
        final void setNext(HashEntry<K, V> n) {
            UNSAFE.putOrderedObject(this, nextOffset, n);
        }
    

JDK1.8 ConcurrentHashMap

和HashMap類似,相較於jdk1.7在jdk1.8中引入了紅黑樹

圖示結構

圖片來源於https://www.javadoop.com/post/hashmap圖片來源於https://www.javadoop.com/post/hashmap


初始化

//====================先初步分析這兩個構造函數,構造函數還有其他...================================
    /**
     * Creates a new, empty map with the default initial table size (16).
     */
    public ConcurrentHashMap() {
    }
    
    public ConcurrentHashMap(int initialCapacity) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException();
        int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
                   MAXIMUM_CAPACITY :
                   tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
        //sizeCtl = 【 (1.5 * initialCapacity + 1),然後向上取最近的 2 的 n 次方】。
        // 如 initialCapacity 爲 10,那麼得到 sizeCtl 爲 16,如果 initialCapacity 爲 11,得到 sizeCtl 爲 32。
        this.sizeCtl = cap;
    }

put過程

    public V put(K key, V value) {
        return putVal(key, value, false);
    }

    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        //得到key的hash值
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            //數組爲空,則初始化數組
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            //數組對應位置元素爲空,則利用CAS操作將新值放入其中,結束for循環
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            //和擴容相關
            else if ((fh = f.hash) == MOVED)
                //幫助轉移數據
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                //邏輯到這裏,說明f是數組該位置的頭結點且不爲空
                //插入結點的過程中用到了synchronized保證獲取鎖
                synchronized (f) {//f = tabAt(tab, i = (n - 1) & hash))
                    if (tabAt(tab, i) == f) {
                        //fh = f.hash   頭結點的hash值大於0,說明爲鏈表
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                //找到相同的key則進行舊值替換
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                //沒找到相同的key則將新結點插入到鏈表的末尾
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        //數組對應位置的結點是紅黑樹結點
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            //調用紅黑樹的插值方法插入新的結點
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    //TREEIFY_THRESHOLD(桶的樹形化閾值):8
                    if (binCount >= TREEIFY_THRESHOLD)
                        //同HashMap1.8, 都是要先判斷數組長度是否大於最小樹形化閾值(默認64),小於則嘗試擴容而不是樹形化
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }


/**
  * 樹形化
  */
    private final void treeifyBin(Node<K,V>[] tab, int index) {
        Node<K,V> b; int n, sc;
        if (tab != null) {
            //MIN_TREEIFY_CAPACITY(最小樹形化閾值):64
            if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
                tryPresize(n << 1);
            //b是頭結點
            else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
                //加鎖
                synchronized (b) {
                    //b爲鏈表頭結點
                    if (tabAt(tab, index) == b) {
                        TreeNode<K,V> hd = null, tl = null;
                        //遍歷鏈表,建立一顆紅黑樹
                        for (Node<K,V> e = b; e != null; e = e.next) {
                            TreeNode<K,V> p =
                                new TreeNode<K,V>(e.hash, e.key, e.val,
                                                  null, null);
                            if ((p.prev = tl) == null)
                                hd = p;
                            else
                                tl.next = p;
                            tl = p;
                        }
                        //將紅黑樹設置到數組的相應位置上去
                        setTabAt(tab, index, new TreeBin<K,V>(hd));
                    }
                }
            }
        }
    }

put過程中涉及到了初始化數組擴容幫助數據遷移三個重要的過程,下面將一一介紹。


初始化數組

該方法主要就是初始化一個合適大小的數組,然後會設置sizeCtl。
初始化方法中的併發問題是通過對sizeCtl進行一個CAS操作來完成的。

   /**
     * 初始化數組
     * Initializes table, using the size recorded in sizeCtl.
     */
    private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
            //sizeCtl小於0,說明有其他線程正在對table進行初始化或者擴容,使用yield進行等待
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // lost initialization race; just spin
            //邏輯到這裏,說明sizeCtl>=0,CAS將sizeCtl設爲-1,代表獲取了鎖
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;//DEFAULT_CAPACITY,默認數組長度爲16
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        //table是volatile的
                        table = tab = nt;
                        //sc=0.75 * n;
                        sc = n - (n >>> 2);
                    }
                } finally {
                    //將sc賦給sizeCtl
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }


擴容

擴容操作tryPresize中涉及到了數據遷移transfer方法,這兩個方法也是 Java8 ConcurrentHashMap中比較複雜的方法

	// 首先要說明的是,方法參數 size 傳進來的時候就已經翻了倍了
    private final void tryPresize(int size) {
        //c: 【1.5倍的size再加1,再往上取最近的2的n次方】
        int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
            tableSizeFor(size + (size >>> 1) + 1);
        int sc;
        while ((sc = sizeCtl) >= 0) {
            Node<K,V>[] tab = table; int n;
            //這個if分支和初始化數組的代碼基本一致,可以略過
            if (tab == null || (n = tab.length) == 0) {
                n = (sc > c) ? sc : c;
                if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                    try {
                        if (table == tab) {
                            @SuppressWarnings("unchecked")
                            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                            table = nt;
                            sc = n - (n >>> 2);
                        }
                    } finally {
                        sizeCtl = sc;
                    }
                }
            }
            //數組長度大於允許的最大長度2^30,或者擴容的新長度<sc,則什麼都不做,直接返回
            else if (c <= sc || n >= MAXIMUM_CAPACITY)
                break;
            //下面的過程用到了數據遷移transfer
            else if (tab == table) {
                int rs = resizeStamp(n);
                if (sc < 0) {
                    Node<K,V>[] nt;
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;
                    // 2. 用 CAS 將 sizeCtl 加 1,然後執行 transfer 方法
                    //    此時 nextTab 不爲 null
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                }
                // 將 sizeCtl 設置爲 (rs << RESIZE_STAMP_SHIFT) + 2),此時nextTab 爲null
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
            }
        }
    }

這個方法的核心在於 sizeCtl 值的操作,首先將其設置爲一個負數,然後執行 transfer(tab, null),再下一個循環將 sizeCtl 加 1,並執行 transfer(tab, nt),之後可能是繼續 sizeCtl 加 1,並執行 transfer(tab, nt)。
所以,可能的操作就是執行 1 次 transfer(tab, null) + 多次 transfer(tab, nt),這裏怎麼結束循環的需要看完 transfer 源碼才清楚。


數據遷移

下面這個方法有點長,將原來的 tab 數組的元素遷移到新的 nextTab 數組中。

雖然我們之前說的 tryPresize 方法中多次調用 transfer 不涉及多線程,但是這個 transfer 方法可以在其他地方被調用,典型地,我們之前在說 put 方法的時候就說過了,請往上看 put 方法,是不是有個地方調用了 helpTransfer 方法,helpTransfer 方法會調用 transfer 方法的。

此方法支持多線程執行,外圍調用此方法的時候,會保證第一個發起數據遷移的線程,nextTab 參數爲 null,之後再調用此方法的時候,nextTab 不會爲 null。

閱讀源碼之前,先要理解併發操作的機制。原數組長度爲 n,所以我們有 n 個遷移任務,讓每個線程每次負責一個小任務是最簡單的,每做完一個任務再檢測是否有其他沒做完的任務,幫助遷移就可以了,而 Doug Lea 使用了一個 stride,簡單理解就是步長,每個線程每次負責遷移其中的一部分,如每次遷移 16 個小任務。所以,我們就需要一個全局的調度者來安排哪個線程執行哪幾個任務,這個就是屬性 transferIndex 的作用

第一個發起數據遷移的線程會將 transferIndex 指向原數組最後的位置,然後從後往前的 stride 個任務屬於第一個線程,然後將 transferIndex 指向新的位置,再往前的 stride 個任務屬於第二個線程,依此類推。當然,這裏說的第二個線程不是真的一定指代了第二個線程,也可以是同一個線程,這個讀者應該能理解吧。其實就是將一個大的遷移任務分爲了一個個任務包。

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;

    // stride 在單核下直接等於 n,多核模式下爲 (n>>>3)/NCPU,最小值是 16
    // stride 可以理解爲”步長“,有 n 個位置是需要進行遷移的,
    //   將這 n 個任務分爲多個任務包,每個任務包有 stride 個任務
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range

    // 如果 nextTab 爲 null,先進行一次初始化
    //    前面我們說了,外圍會保證第一個發起遷移的線程調用此方法時,參數 nextTab 爲 null
    //       之後參與遷移的線程調用此方法時,nextTab 不會爲 null
    if (nextTab == null) {
        try {
            // 容量翻倍
            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
            nextTab = nt;
        } catch (Throwable ex) {      // try to cope with OOME
            sizeCtl = Integer.MAX_VALUE;
            return;
        }
        // nextTable 是 ConcurrentHashMap 中的屬性
        nextTable = nextTab;
        // transferIndex 也是 ConcurrentHashMap 的屬性,用於控制遷移的位置
        transferIndex = n;
    }

    int nextn = nextTab.length;

    // ForwardingNode 翻譯過來就是正在被遷移的 Node
    // 這個構造方法會生成一個Node,key、value 和 next 都爲 null,關鍵是 hash 爲 MOVED
    // 後面我們會看到,原數組中位置 i 處的節點完成遷移工作後,
    //    就會將位置 i 處設置爲這個 ForwardingNode,用來告訴其他線程該位置已經處理過了
    //    所以它其實相當於是一個標誌。
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);


    // advance 指的是做完了一個位置的遷移工作,可以準備做下一個位置的了
    boolean advance = true;
    boolean finishing = false; // to ensure sweep before committing nextTab

    /*
     * 下面這個 for 循環,最難理解的在前面,而要看懂它們,應該先看懂後面的,然後再倒回來看
     * 
     */

    // i 是位置索引,bound 是邊界,注意是從後往前
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;

        // 下面這個 while 真的是不好理解
        // advance 爲 true 表示可以進行下一個位置的遷移了
        //   簡單理解結局:i 指向了 transferIndex,bound 指向了 transferIndex-stride
        while (advance) {
            int nextIndex, nextBound;
            if (--i >= bound || finishing)
                advance = false;

            // 將 transferIndex 值賦給 nextIndex
            // 這裏 transferIndex 一旦小於等於 0,說明原數組的所有位置都有相應的線程去處理了
            else if ((nextIndex = transferIndex) <= 0) {
                i = -1;
                advance = false;
            }
            else if (U.compareAndSwapInt
                     (this, TRANSFERINDEX, nextIndex,
                      nextBound = (nextIndex > stride ?
                                   nextIndex - stride : 0))) {
                // 看括號中的代碼,nextBound 是這次遷移任務的邊界,注意,是從後往前
                bound = nextBound;
                i = nextIndex - 1;
                advance = false;
            }
        }
        if (i < 0 || i >= n || i + n >= nextn) {
            int sc;
            if (finishing) {
                // 所有的遷移操作已經完成
                nextTable = null;
                // 將新的 nextTab 賦值給 table 屬性,完成遷移
                table = nextTab;
                // 重新計算 sizeCtl:n 是原數組長度,所以 sizeCtl 得出的值將是新數組長度的 0.75 倍
                sizeCtl = (n << 1) - (n >>> 1);
                return;
            }

            // 之前我們說過,sizeCtl 在遷移前會設置爲 (rs << RESIZE_STAMP_SHIFT) + 2
            // 然後,每有一個線程參與遷移就會將 sizeCtl 加 1,
            // 這裏使用 CAS 操作對 sizeCtl 進行減 1,代表做完了屬於自己的任務
            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                // 任務結束,方法退出
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    return;

                // 到這裏,說明 (sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT,
                // 也就是說,所有的遷移任務都做完了,也就會進入到上面的 if(finishing){} 分支了
                finishing = advance = true;
                i = n; // recheck before commit
            }
        }
        // 如果位置 i 處是空的,沒有任何節點,那麼放入剛剛初始化的 ForwardingNode ”空節點“
        else if ((f = tabAt(tab, i)) == null)
            advance = casTabAt(tab, i, null, fwd);
        // 該位置處是一個 ForwardingNode,代表該位置已經遷移過了
        else if ((fh = f.hash) == MOVED)
            advance = true; // already processed
        else {
            // 對數組該位置處的結點加鎖,開始處理數組該位置處的遷移工作
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    Node<K,V> ln, hn;
                    // 頭結點的 hash 大於 0,說明是鏈表的 Node 節點
                    if (fh >= 0) {
                        // 下面這一塊和 Java7 中的 ConcurrentHashMap 遷移是差不多的,
                        // 需要將鏈表一分爲二,
                        //   找到原鏈表中的 lastRun,然後 lastRun 及其之後的節點是一起進行遷移的
                        //   lastRun 之前的節點需要進行克隆,然後分到兩個鏈表中
                        int runBit = fh & n;
                        Node<K,V> lastRun = f;
                        for (Node<K,V> p = f.next; p != null; p = p.next) {
                            int b = p.hash & n;
                            if (b != runBit) {
                                runBit = b;
                                lastRun = p;
                            }
                        }
                        if (runBit == 0) {
                            ln = lastRun;
                            hn = null;
                        }
                        else {
                            hn = lastRun;
                            ln = null;
                        }
                        for (Node<K,V> p = f; p != lastRun; p = p.next) {
                            int ph = p.hash; K pk = p.key; V pv = p.val;
                            if ((ph & n) == 0)
                                ln = new Node<K,V>(ph, pk, pv, ln);
                            else
                                hn = new Node<K,V>(ph, pk, pv, hn);
                        }
                        // 其中的一個鏈表放在新數組的位置 i
                        setTabAt(nextTab, i, ln);
                        // 另一個鏈表放在新數組的位置 i+n
                        setTabAt(nextTab, i + n, hn);
                        // 將原數組該位置處設置爲 fwd,代表該位置已經處理完畢,
                        //    其他線程一旦看到該位置的 hash 值爲 MOVED,就不會進行遷移了
                        setTabAt(tab, i, fwd);
                        // advance 設置爲 true,代表該位置已經遷移完畢
                        advance = true;
                    }
                    else if (f instanceof TreeBin) {
                        // 紅黑樹的遷移
                        TreeBin<K,V> t = (TreeBin<K,V>)f;
                        TreeNode<K,V> lo = null, loTail = null;
                        TreeNode<K,V> hi = null, hiTail = null;
                        int lc = 0, hc = 0;
                        for (Node<K,V> e = t.first; e != null; e = e.next) {
                            int h = e.hash;
                            TreeNode<K,V> p = new TreeNode<K,V>
                                (h, e.key, e.val, null, null);
                            if ((h & n) == 0) {
                                if ((p.prev = loTail) == null)
                                    lo = p;
                                else
                                    loTail.next = p;
                                loTail = p;
                                ++lc;
                            }
                            else {
                                if ((p.prev = hiTail) == null)
                                    hi = p;
                                else
                                    hiTail.next = p;
                                hiTail = p;
                                ++hc;
                            }
                        }
                        // 如果一分爲二後,節點數少於 8,那麼將紅黑樹轉換回鏈表
                        ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                            (hc != 0) ? new TreeBin<K,V>(lo) : t;
                        hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                            (lc != 0) ? new TreeBin<K,V>(hi) : t;

                        // 將 ln 放置在新數組的位置 i
                        setTabAt(nextTab, i, ln);
                        // 將 hn 放置在新數組的位置 i+n
                        setTabAt(nextTab, i + n, hn);
                        // 將原數組該位置處設置爲 fwd,代表該位置已經處理完畢,
                        //    其他線程一旦看到該位置的 hash 值爲 MOVED,就不會進行遷移了
                        setTabAt(tab, i, fwd);
                        // advance 設置爲 true,代表該位置已經遷移完畢
                        advance = true;
                    }
                }
            }
        }
    }
}

transfer方法並沒有實現所有的遷移任務,每次調用這個方法只是實現了transferIndex往前stride個位置的遷移工作,其他的需要由外圍來控制,即tryPrize方法。


get過程

    public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        //1. 獲得hash值
        int h = spread(key.hashCode());
        //2. 根據hash值定位到數組具體位置
        //2.1  具體位置爲null,則直接返回null
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
            //2.2 數組對應位置正好是要找的元素,則直接返回該元素
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            //2.3 對應位置hash值小於0,說明正在擴容或者是紅黑樹,繼續調用find方法查找,  參考 ForwardingNode.find(int h, Object k) 和 TreeBin.find(int h, Object k)

            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
            //2.4 對應位置hash值正常,則遍歷鏈表查找
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }


總結

JDK1.7ConcurrentHashMap:

  • 初始化: 懶加載,創建了一個Segment數組並初始化了Segment[0]
  • put過程:
  1. 校驗value不能爲空,爲空則直接拋空指針異常 key可以空 !!1.8版本key和value都不能爲空
  2. 根據key得到hash值,然後根據hash值找到Segment數組中的位置j
  3. 若Segment[j]爲null,則根據Segment[0]爲原型對Segment[j]進行初始化,初始化的過程中使用循環CAS確保線程安全
  4. 在Segment[j]中將新元素插入到合適位置,這個過程首先要獲得獨佔鎖,Segment 直接繼承了 ReentrantLock,所以可以直接調用tryLock方法獲得鎖。由於Segment的數據結構就是數組+鏈表,所以插入過程和JDK1.7的HashMap很像,比較大的區別就是ConcurrentHashMap通過獲取獨佔鎖保證了線程安全。
  • get過程:
  1. 先根據key的hash值找到對應的Segment
  2. 再根據 hash 找到數組中具體的位置
  3. 到這裏是鏈表了,順着鏈表進行查找即可
  • 擴容過程rehash:
    擴容時機:當向某個Segment中插入具體元素前,發現如果插入元素後這個Segment的元素超過了閾值且元素數量少於允許的最大容量,則先針對這個Segment進行擴容,然後再插入新元素。因此,擴容是針對Segment的擴容
  1. 創建舊table長度2倍大小的新數組
  2. 重新計算原數組每個元素在新數組中的位置
    2.1 如果舊數組對應位置只有一個元素,即鏈表只有一個結點,則直接將元素放在新數組中;
    2.2 否則對每個鏈表,從鏈表中找到一個lastRun節點,這個節點之後的所有元素和這個節點在新數組的下標相同
    2.2.1 將 lastRun 及其之後的所有節點組成的這個鏈表放到 新數組對應的下標位置上
    2.2.2 將lastRun之前的結點一一利用頭插法插入到新數組對應的下標位置上
  3. 舊table的所有元素重哈希遷移完畢後,新的結點根據hash值計算在新數組具體的下標位置,然後插入到對應的鏈表頭部。

JDK1.8ConcurrentHashMap: 相比較1.7引入了紅黑樹

  • 初始化:懶加載,在第一次put操作時纔會真正初始化數組。通過CAS操作控制sizeCtl來保證線程的安全性。當sizeCtl<0時,說明有其他線程正在初始化或者正在擴容,調用Thread.yield()方法讓給其他線程;當sizeCtl>=0時則CAS將 sizeCtl 設置爲 -1,代表搶到了鎖
    /**
     * Table initialization and resizing control.  When negative, the
     * table is being initialized or resized: -1 for initialization,
     * else -(1 + the number of active resizing threads).  Otherwise,
     * when table is null, holds the initial table size to use upon
     * creation, or 0 for default. After initialization, holds the
     * next element count value upon which to resize the table.
     */
    private transient volatile int sizeCtl;
  • put過程:
  1. key和value都不能爲空,爲空則拋空指針異常
  2. 根據key得到hash值
  3. 數組爲空則進行初始化
  4. 找hash值對應的數組下標,得到第一個結點f
    3.1 如果數組對應位置爲空即f爲null,則直接CAS將新值放入其中即可
    3.2 如果f.hash等於-1,說明正在擴容則幫助數據遷移
    3.3 上述兩種情況都不滿足則使用synchronized獲取數組該位置的頭結點的監視器鎖
    3.3.1 如果f是鏈表結點,則找到相同的key則進行舊值覆蓋;找不到則將新值結點加到鏈表末尾
    3.3.2 如果f是樹結點,則調用紅黑樹的插值方法插入新節點
  5. 插入後根據桶的樹形化閾值判斷是否需要樹形化(在樹形化 中的第一步是判斷數組長度是否達到最小樹形化閾值,若沒有則擴容而不是樹形化)
  • get過程:
  1. 計算 hash 值
  2. 根據 hash 值找到數組對應位置: (n - 1) & h
  3. 根據該位置處結點性質進行相應查找
    3.1 如果該位置爲 null,那麼直接返回 null 就可以了
    3.2 如果該位置處的節點剛好就是我們需要的,返回該節點的值即可
    3.3 如果該位置節點的 hash 值小於 0,說明正在擴容,或者是紅黑樹,調用相應的find方法查找
    3.4 如果以上 3 條都不滿足,那就是鏈表,進行遍歷比對即可
  • 擴容過程:翻倍擴容,擴容後數組容量爲原來的 2 倍。
  • 數據遷移:原數組長度爲 n,所以我們有 n 個遷移任務,讓每個線程每次負責一個小任務是最簡單的,每做完一個任務再檢測是否有其他沒做完的任務,幫助遷移就可以了。Doug Lea使用了一個stride,可以理解爲步長,每個線程每次負責遷移一部分。第一個發起數據遷移的線程將transferIndex指向原數組最後的位置,然後從後往前 的stride個任務屬於第一個線程,再往前stride個任務屬於第二個線程,以此類推。思想就是將一個大的遷移任務分爲了一個個任務包。

參考

https://www.javadoop.com/post/hashmap

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