ConcurrentHashMap 1.8 源碼解讀 (面試殺手鐗)

1.本文簡介

1.1 ConcurrentHashMap put方法介紹

2.源碼解讀

2.1 put方法解讀

調用put(K key, V value)方法實際調用的是putVal(key, value, false),如下:

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

所以,解讀重點在putVal方法上。先看一下putVal方法操作主要流程:

1.若 key == null || value == null,拋出 NPE,結束;否則,計算key的hash值,繼續往下執行

2.對存放<key,value>的 Node<K,V>[] tab 數組進行for循環遍歷,for (Node<K,V>[] tab = table;;),for循環退出交由執行體控制;

執行體進行如下一系列判斷:

    2.1 判斷tab是否初始化,未初始化則進行初始化(首次初始化默認數組長度爲16);

 if (tab == null || (n = tab.length) == 0)
    tab = initTable();

    2.2 判斷key對應的數組位置上是否爲null,若尚未發生hash碰撞,即進行CAS操作,new 一個 Node<K,V>存放到tab中,退出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
}

    2.3 判斷tab是否需要進行擴容:

 else if ((fh = f.hash) == MOVED)  // MOVED = -1
    tab = helpTransfer(tab, f);

    2.4 以上判斷都未進入,說明tab已初始化,且有容量存放數據,但發生了hash衝突,接下來則進入hash衝突解決階段:

         先用synchronized 鎖住f,即tab數組發生hash衝突位置上的元素對象Node,然後再判斷一下tabAt(tab, i) == f,主要作用是tab是一個volatile修飾的對所有線程可見的共享變量,在synchronized 上鎖成功之前其他線程有可能對tab做了操作,如擴容,remove等。

        2.4.1 if (fh >= 0) 當做鏈表處理:進行for循環,若hash 和 key 都相同,則用新值替換舊值,break;否則一直循環到鏈表尾,當 (e = e.next) == null 時,pred.next = new Node<K,V>,添加數據到鏈表,break; 注意此處 ++binCount,binCount用來統計鏈表長度;

        2.4.2 else if (f instanceof TreeBin) 當做樹結構處理:若添加的key不存在於樹中,則 putTreeVal 方法直接添加並返回null,若是key對應的數據已存在,putTreeVal 方法返回樹中的Node<K,V> p,並用新值替換舊值;

        2.4.3 在synchronized 代碼塊執行結束後,判斷鏈表長度是否 >=  閾值 8(TREEIFY_THRESHOLD),是則轉爲紅黑樹;

之後要麼 return oldVal ,要麼break 結束for循環;

else {
    V oldVal = null;
    synchronized (f) {
        if (tabAt(tab, i) == f) {
            if (fh >= 0) {
                binCount = 1;
                for (Node<K,V> e = f;; ++binCount) {
                    K ek;
                    if (e.hash == hash &&
                        ((ek = e.key) == key ||
                         (ek != null && key.equals(ek)))) {    // key 存在,更新
                        oldVal = e.val;
                        if (!onlyIfAbsent)
                            e.val = value;
                        break;
                    }
                    Node<K,V> pred = e;
                    if ((e = e.next) == null) {
                        pred.next = new Node<K,V>(hash, key,
                                                  value, null);    //key 不存在,鏈表中追加新元素
                        break;
                    }
                }
            }
            else if (f instanceof TreeBin) {
                Node<K,V> p;
                binCount = 2;
                //key不存在則putTreeVal方法直接添加新元素並返回null,key存在則返回對應節點p並做val更新
                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)
            treeifyBin(tab, i);
        if (oldVal != null)
            return oldVal;
        break;
    }
}

3. for 循環處理結束後,檢查tab是否需要擴容:

  addCount(1L, binCount);   //檢查擴容

4. putVal 方法執行結束: return null;

2.2 put 方法流程圖

    爲更直觀瞭解put方法的執行過程,畫個流程圖:

ConcurrentHashMap  put 流程圖

 2.3 細節探討

    2.3.1 鏈表轉紅黑樹

    1. 鏈表轉紅黑樹閾值 TREEIFY_THRESHOLD 8 的問題,看源碼,是用binCount來比較;但binCount並不能完全正確的及時反映鏈表的長度。看下面這段代碼:

synchronized (f) {
    if (tabAt(tab, i) == f) {
        if (fh >= 0) {
            binCount = 1;
            for (Node<K,V> e = f;; ++binCount) {
                K ek;
                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;
                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) {
    if (binCount >= TREEIFY_THRESHOLD)
        treeifyBin(tab, i);
    if (oldVal != null)
        return oldVal;
    break;
}

(1)假設本次key存在,且在鏈表的中間位置,那麼break的時候 binCount 不能代表鏈表的實際長度,此時實際長度可能已達             到8;

(2)假設本次key不存在,且鏈表長度已經爲7,那麼添加新元素後,實際鏈表長度已經是8,但break時,binCount 還是7;

(3)至於閾值爲什麼爲8,哈哈,不清楚哈,估計是基於測試統計給出來的吧。有個 泊松分佈 概率函數,感興趣的可以瞭解              下。

  2. 鏈表轉紅黑樹,關於tab 數組長度的問題。看代碼:

/**
 * Replaces all linked nodes in bin at given index unless table is
 * too small, in which case resizes instead.
 */
private final void treeifyBin(Node<K,V>[] tab, int index) {
    Node<K,V> b; int n, sc;
    if (tab != null) {
        if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
            tryPresize(n << 1);    //MIN_TREEIFY_CAPACITY 默認64,tryPresize 擴容
        else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
            synchronized (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));
                }
            }
        }
    }
}

 (1)MIN_TREEIFY_CAPACITY 爲64,可見,當鏈表長度達到8但tab長度小於64時,並不會直接進行紅黑樹轉化,而是對tab進行擴容,根據新的new_tab大小以及key的hash值,重新分配index,將原有元素copy到新的new_tab中,且原有元素若爲鏈表,擴容後理論上分爲2個小鏈表,鏈表位置爲 index=i 和 index=i+n ,n 表示原有tab大小;若原有元素爲樹結構,擴容後理論上分爲2個小樹,樹位置爲 index=i 和 index=i+n ,n 表示原有tab大小,若小樹中元素小於等於UNTREEIFY_THRESHOLD 6 ,擴容時會向下轉化爲鏈表,否則繼續生成樹結構;由此也可見,當鏈表長度達到8但tab長度小於64時,採用的是擴容來降低hash衝突,從而理論上降低鏈表長度。

3.小結

3.1 ConcurrentHashMap 1.8 put 方法主要採用 CAS (hash未衝突使用) + synchronized (hash衝突使用),來解決併發時的線程安全問題;

3.2 鏈表轉爲紅黑樹,必要條件是  數組長度不小於64,且鏈表長度不小於8;

4.附完整源碼

/**
 * Maps the specified key to the specified value in this table.
 * Neither the key nor the value can be null.
 *
 * <p>The value can be retrieved by calling the {@code get} method
 * with a key that is equal to the original key.
 *
 * @param key key with which the specified value is to be associated
 * @param value value to be associated with the specified key
 * @return the previous value associated with {@code key}, or
 *         {@code null} if there was no mapping for {@code key}
 * @throws NullPointerException if the specified key or value is null
 */
public V put(K key, V value) {
    return putVal(key, value, false);
}

/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    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();
        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;
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            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;
                            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) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

評論區歡迎討論!覺得有用點個贊再走吧。感謝!!!

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