源碼分析 CurrentHashMap 1.8


1.0 數據結構

  

 

  • 拋棄了 JDK 1.7 中原有的 Segment 分段鎖,而采用了 CAS + synchronized 來保證並發安全性
  • 將 JDK 1.7 中存放數據的 HashEntry 改為 Node,但作用是相同的

2.0 put方法

final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException(); // 鍵或值為空,拋出異常
        // 鍵的hash值經過計算獲得hash值,這里的 hash 計算多了一步 & HASH_BITS,HASH_BITS 是 0x7fffffff,該步是為了消除最高位上的負符號 hash的負在ConcurrentHashMap中有特殊意義表示在擴容或者是樹結點
        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) // 表為空或者表的長度為0
                // 初始化表
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // 表不為空並且表的長度大於0,並且該桶不為空
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null))) // 比較並且交換值,如tab的第i項為空則用新生成的node替換
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED) // 該結點的hash值為MOVED
                // 進行結點的轉移(在擴容的過程中)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                synchronized (f) { // 加鎖同步
                    if (tabAt(tab, i) == f) { // 找到table表下標為i的結點
                        if (fh >= 0) { // 該table表中該結點的hash值大於0
                            // binCount賦值為1
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) { // 無限循環
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) { // 結點的hash值相等並且key也相等
                                    // 保存該結點的val值
                                    oldVal = e.val;
                                    if (!onlyIfAbsent) // 進行判斷
                                        // 將指定的value保存至結點,即進行了結點值的更新
                                        e.val = value;
                                    break;
                                }
                                // 保存當前結點
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) { // 當前結點的下一個結點為空,即為最后一個結點
                                    // 新生一個結點並且賦值給next域
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    // 退出循環
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) { // 結點為紅黑樹結點類型
                            Node<K,V> p;
                            // binCount賦值為2
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) { // 將hash、key、value放入紅黑樹
                                // 保存結點的val
                                oldVal = p.val;
                                if (!onlyIfAbsent) // 判斷
                                    // 賦值結點value值
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) { // binCount不為0
                    if (binCount >= TREEIFY_THRESHOLD) // 如果binCount大於等於轉化為紅黑樹的閾值
                        // 進行轉化
                        treeifyBin(tab, i);
                    if (oldVal != null) // 舊值不為空
                        // 返回舊值
                        return oldVal;
                    break;
                }
            }
        }
        // 增加binCount的數量
        addCount(1L, binCount);
        return null;
}

put方法總結

  1. 判斷存儲的 key、value 是否為空,若為空,則拋出異常,否則,進入步驟 2。
  2. 計算 key 的 hash 值,隨后進入自旋,該自旋可以確保成功插入數據,若 table 表為空或者長度為 0,則初始化 table 表,否則,進入步驟 3。
  3. 根據 key 的 hash 值取出 table 表中的結點元素,若取出的結點為空(該桶為空),則使用 CAS 將 key、value、hash 值生成的結點放入桶中。否則,進入步驟 4。
  4. 若該結點的的 hash 值為 MOVED(-1),則對該桶中的結點進行轉移,否則,進入步驟 5。
  5. 對桶中的第一個結點(即 table 表中的結點)進行加鎖,對該桶進行遍歷,桶中的結點的 hash 值與 key 值與給定的 hash 值和 key 值相等,則根據標識選擇是否進行更新操作(用給定的 value 值替換該結點的 value 值),若遍歷完桶仍沒有找到 hash 值與 key 值和指定的 hash 值與 key 值相等的結點,則直接新生一個結點並賦值為之前最后一個結點的下一個結點。進入步驟 6。
  6. 若 binCount 值達到紅黑樹轉化的閾值,則將桶中的結構轉化為紅黑樹存儲,最后,增加 binCount 的值。
  • 如果桶中的第一個元素的 hash 值大於 0,說明是鏈表結構,則對鏈表插入或者更新。
  • 如果桶中的第一個元素是 TreeBin,說明是紅黑樹結構,則按照紅黑樹的方式進行插入或者更新。
  • 在鎖的保護下,插入或者更新完畢后,如果是鏈表結構,需要判斷鏈表中元素的數量是否超過 8(默認),一旦超過,就需要考慮進行數組擴容,或者是鏈表轉紅黑樹。

3.0 初始化

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
        // 初始化數組的工作其它線程正在做
        if ((sc = sizeCtl) < 0)
            Thread.yield(); // lost initialization race; just spin
        // CAS 一下,將 sizeCtl 設置為 -1,代表搶到了鎖
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                if ((tab = table) == null || tab.length == 0) {
                    // DEFAULT_CAPACITY 默認初始容量是 16
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    // 初始化數組,長度為 16 或初始化時提供的長度
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    // 將這個數組賦值給 table,table 是 volatile 的
                    table = tab = nt;
                    // 如果 n 為 16 的話,那么這里 sc = 12
                    // 其實就是 0.75 * n
                    sc = n - (n >>> 2);
                }
            } finally {
                // 設置 sizeCtl 為 sc
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

4.0 鏈表轉紅黑樹

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) // table表的長度小於最小的長度
                // 進行擴容,調整某個桶中結點數量過多的問題(由於某個桶中結點數量超出了閾值,則觸發treeifyBin)
                tryPresize(n << 1);
            else if ((b = tabAt(tab, index)) != null && b.hash >= 0) { // 桶中存在結點並且結點的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結點
                            TreeNode<K,V> p =
                                new TreeNode<K,V>(e.hash, e.key, e.val,
                                                  null, null);
                            if ((p.prev = tl) == null) // 該結點前驅為空
                                // 設置p為頭結點
                                hd = p;
                            else
                                // 尾結點的next域賦值為p
                                tl.next = p;
                            // 尾結點賦值為p
                            tl = p;
                        }
                        // 設置table表中下標為index的值為hd
                        setTabAt(tab, index, new TreeBin<K,V>(hd));
                    }
                }
            }
        }
}

數組擴容

// 參數 size 傳進來的時候就已經翻倍(例如 16)
private final void tryPresize(int size) {
    // c:size 的 1.5 倍,再加 1,再往上取最近的 2 的 n 次方。
    // 16 + 8 + 1 -> 32 -> 2^8
    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); // 0.75 * n
                    }
                } finally {
                    sizeCtl = sc;
                }
            }
        }
        else if (c <= sc || n >= MAXIMUM_CAPACITY)
            break;
        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);
            }
            // 1. 將 sizeCtl 設置為 (rs << RESIZE_STAMP_SHIFT) + 2)
            //  調用 transfer 方法,此時 nextTab 參數為 null
            else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                         (rs << RESIZE_STAMP_SHIFT) + 2))
                transfer(tab, null);
        }
    }
}

5.0 get方法

public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    int h = spread(key.hashCode());
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        // 判斷頭結點是否就是我們需要的結點
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        // 如果頭結點的 hash 小於 0,說明 正在擴容,或者該位置是紅黑樹
        else if (eh < 0)
            // 參考 ForwardingNode.find(int h, Object k) 和 TreeBin.find(int h, Object k)
            return (p = e.find(h, key)) != null ? p.val : null;
 
        // 遍歷鏈表
        while ((e = e.next) != null) {
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}

6.0 其他問題

  

6.2.1 ConcurrentHashmap 不支持 key 或者 value 為 null 的原因

  • ConcurrentHashmap 和 Hashtable 都是支持並發的,當通過 get(k) 獲取對應的 value 時,如果獲取到的是 null 時,無法判斷是 put(k,v) 的時候 value 為 null,還是這個 key 從來沒有做過映射。
    • HashMap 是非並發的,可以通過 contains(key) 來做這個判斷。
    • 支持並發的 Map 在調用 m.contains(key) 和 m.get(key) 時,m 可能已經發生了更改。
  • 因此 ConcurrentHashmap 和 Hashtable 都不支持 key 或者 value 為 null。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM