集合類源碼(七)Map(ConcurrentHashMap, ConcurrentSkipListMap, TreeMap)

ConcurrentHashMap

內部結構

在JDK1.8之前的實現結構是:ReentrantLock+Segment+HashEntry+鏈表

JDK1.8之後的實現結構是:synchronized+CAS+Node+鏈表或紅黑樹(與HashMap一致)

而1.8之前鎖的是Segment,1.8鎖的是Node數組裏的Node,準確來說是頭結點。如圖虛線所示:

 爲什麼要廢棄鎖分段機制:

1. 分段造成內存浪費(內存不連續,碎片化)

2. 在添加時競爭同一個鎖的概率非常小,分段鎖反而會造成更新等操作的長時間等待;並且當某個段很大時,分段鎖的性能會下降。

3. 爲了提高 GC 的效率

 爲什麼加鎖不用ReentrantLock而是用synchronized:

1. 鎖的細化,之前ReentrantLock鎖住的是整個段,現在synchronized鎖住的是單個Node。

2. 因爲鎖的細化,出現競爭的情況大大減少。

3. 如果競爭同一個Node,只要線程可以在自旋有限次數內拿到鎖,Synchronized就不會升級爲重量級鎖,而等待的線程也就不用被掛起,我們也就少了掛起和喚醒這個上下文切換的過程開銷;而ReentrantLock不會自旋,只會掛起,多了個上下文切換的開銷。

爲什麼容量最好爲2的冪:

當數組長度爲2的n次冪的時候,不同的key算得hash相同的機率較小,那麼數據在數組上分佈就比較均勻,也就是發生碰撞的機率較小,進而導致鏈表結構減少,查詢的時候不用遍歷鏈表的話查詢效率就高了。

爲什麼get不用加鎖:

前面我畫的圖裏,Node的成員變量val是用volatile關鍵字修飾的,其它線程做出的修改能夠馬上看見,保證每次讀取的都是最新的數據。

源碼

put

final V putVal(K key, V value, boolean onlyIfAbsent) {
    // key和value 不能爲null
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
    // 遍歷Node數組
    for (Node<K,V>[] tab = table;;) {
        // f存儲當前位置數組上的Node,n代表數組長度,i代表當前數組下標,fh代表當前Node的hash值
        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) {
            // 目標位置的值爲null,利用CAS設置value,返回。
            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)
            // 如果hash值等於-1,代表正在擴容,helpTransfer會幫助擴容
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            // 加鎖進入
            synchronized (f) {
                // 再獲取一下當前位置的Node,如果和前面獲取的f不一致則發生了變化,跳出同步塊
                if (tabAt(tab, i) == f) {
                    // fh爲正數,代表鏈表結構
                    if (fh >= 0) {
                        binCount = 1;
                        // 遍歷鏈表節點
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            // 如果hash值一樣,並且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;
                            // 如果已經遍歷到了最後(e.next==null),則直接插入到最後
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    // fh < 0代表正在擴容或者紅黑樹結構
                    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;
                            // key衝突,則覆蓋舊值
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            // binCount爲當前位置包含的的Node數量,如果不是0,則判斷是否需要擴容
            if (binCount != 0) {
                // Node數量大於等於8,當前位置的數據類型轉爲樹
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                // 如果oldVal不爲空,證明存在覆蓋的情況,直接返回舊值
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    // 整個Map的Node數量+1,如果需要擴容則進行擴容
    addCount(1L, binCount);
    return null;
}

過程和HashMap類似:

0. Node數組沒有初始化先去初始化;

1. 根據hash找到數組中的位置,如果此位置爲空,則直接利用CAS將新節點放在此處;

2. 如果當前位置不爲空,則判斷此位置的Node的hash是否等於-1,等於-1代表正在進行擴容操作,調用helpTransfer協助擴容;

3. 此位置Node的hash不等於-1,則對其進行加鎖:

4. 如果此位置Node的hash大於等於0,證明這是個鏈表結構,先看是否存在相同的key,有則覆蓋,無則把新結點添加到鏈表最後;

5. 否則判斷當前節點是否是樹節點,如果是樹節點,則添加到樹中,有重複的key同樣會覆蓋;

6. 退出同步塊,判斷binCount(Node計數器)如果大於等於8,則把當前位置的鏈表轉變成紅黑樹;(這裏可以看出,binCount主要服務於鏈表結構,具體位置統計當前鏈表的大小)

7. 最後把整個Map的Node總數+1,如果需要擴容則擴容。

下面看一下initTable【初始化的過程】

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
        // table爲空並且sizeCtl < 0,有其它線程在初始化,則調用Thread.yield(),讓掉自己的CPU執行時間
        if ((sc = sizeCtl) < 0) // 不擴容時:sizeCtl=數組長度*擴容因子;擴容和初始化table時:sizeCtl < 0
            Thread.yield(); // 放棄初始化的競爭,僅僅自旋
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            // sizeCtl > 0,說明沒有線程競爭初始化table,利用CAS將sizeCtl設置爲-1,代表正在初始化
            try {
                // 再次判斷table是否爲空
                if ((tab = table) == null || tab.length == 0) {
                    // 設置table容量,如果sc大於0則使用sc,否則使用默認的16
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    // 根據容量new一個Node數組
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    // 新數組替換老數組
                    table = tab = nt;
                    // sc = 新容量 - (新容量/2^2),無符號右移2位,相當於除以2^2=4
                    // 以16爲例:sc = 16-(16/4)= 16-4 = 12,也就是下一次擴容的閾值
                    sc = n - (n >>> 2);
                }
            } finally {
                // 最後,更新sizeCtl
                sizeCtl = sc;
            }
            break;
        }
    }
    // 返回新數組
    return tab;
}

總結:

1. 根據sizeCtl判斷,如果小於0,表示正在初始化,則讓出當前線程的時間片。

2. 設置sizeCtl爲-1,代表正在執行初始化操作;如果sc存儲的變量大於0,則新容量=sc,否則等於默認容量16;根據新容量new一個新Node數組,並更新table爲新數組;更新sizeCtl爲新容量的75%

再來看一下addCount【Node總數+1 & 擴容的過程】

/**
 * sizeCtl(-1表示table正在初始化,其他線程要讓出CPU時間片;-N表示有N-1個線程正在執行擴容操作;大於0表示擴容閾值=容量*負載因子)
 * @param x 需要加上的數量
 * @param check if <0, don't check resize, if <= 1 only check if uncontended
 */
private final void addCount(long x, int check) {
    // CounterCell:顧名思義,用於計數的格子。說白了就是用來統計table中每一個位置的Node數量。
    CounterCell[] as; long b, s;
    // CounterCell不爲null
    if ((as = counterCells) != null ||
        // 或者利用CAS將baseCount更新爲baseCount+1失敗,就放棄對baseCount的操作
        !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
        CounterCell a; long v; int m;
        boolean uncontended = true;
        if (as == null || (m = as.length - 1) < 0 ||
            (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
            !(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
            fullAddCount(x, uncontended);
            return;
        }
        if (check <= 1)
            return;
        // 合計Node總數,其中的實現是遍歷CounterCell[],累加其中的value
        s = sumCount();
    }
    // check>=0,需要檢查是否需要擴容
    if (check >= 0) {
        // tab:指向table,nt:指向nextTable;n爲當前table的容量,sc爲當前擴容閾值
        Node<K,V>[] tab, nt; int n, sc;
        // Node總數大於擴容閾值sizeCtl 並且 table不爲空 並且 table容量小於最大容量
        while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
               (n = tab.length) < MAXIMUM_CAPACITY) {
            int rs = resizeStamp(n);
            // 如果正在擴容
            if (sc < 0) {
                // 如果sizeCtl變化了或者擴容結束了,則跳出循環
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                    transferIndex <= 0)
                    break;
                // 如果可以幫助擴容,那麼將 sc 加 1. 表示多了一個線程在幫助擴容
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                    transfer(tab, nt);
            }
            // 如果沒有擴容,將 sc 更新爲負數,表示當前線程發起擴容操作
            else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2))
                transfer(tab, null);
            s = sumCount();
        }
    }
}

總結:

1. 使table的長度+1。CounterCell不爲null,就使用CounterCell,否則直接利用CAS操縱baseCount。

2. 如果需要擴容,先看是否已經在擴容了,如果是,則加入擴容線程,否則就調用擴容方法開啓擴容。

最後看transfer方法,這是擴容過程 的核心

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    // n爲當前數組大小,stride存儲步長
    int n = tab.length, stride;
    // 根據cpu核數計算出步長,用於分割擴容任務,方便其餘線程幫助擴容,最小爲16
    // 默認每個線程處理16個桶。因此,如果長度是16的時候,擴容的時候只會有一個線程擴容。
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range
    // 判斷nextTab是否爲空,nextTab是暫時存儲擴容後的node的數組,第一次進入這個方法的線程纔會發現nextTab爲空
    if (nextTab == null) {            // initiating
        try {
            // 初始化nextTab,容量是tab的2倍
            @SuppressWarnings("unchecked")
            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 = nextTab;
        // 當前數組長度賦給transferIndex
        transferIndex = n;
    }
    // nextTab的大小
    int nextn = nextTab.length;
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    boolean advance = true;
    // finishing爲true代表擴容結束
    boolean finishing = false; // to ensure sweep before committing nextTab
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;
        // 進入一個 while 循環,分配數組中一個桶的區間給線程. 從大到小進行分配。當拿到分配值後,進行 i-- 遞減。這個 i 就是數組下標。
        while (advance) {
            int nextIndex, nextBound;
            if (--i >= bound || finishing)
                advance = false;
            else if ((nextIndex = transferIndex) <= 0) {
                i = -1;
                advance = false;
            }
            else if (U.compareAndSwapInt
                     (this, TRANSFERINDEX, nextIndex,
                      nextBound = (nextIndex > stride ?
                                   nextIndex - stride : 0))) {
                bound = nextBound;
                i = nextIndex - 1;
                advance = false;
            }
        }

        if (i < 0 || i >= n || i + n >= nextn) {
            int sc;
            // 如果擴容結束
            if (finishing) {
                // 清除臨時變量
                nextTable = null;
                // 更新table變量
                table = nextTab;
                // 更新sizeCtl,這個等價於新容量*0.75
                sizeCtl = (n << 1) - (n >>> 1);
                return;
            }
            // 嘗試將 sc -1. 表示這個線程結束幫助擴容了
            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                // 果 sc - 2 不等於標識符左移 16 位。如果他們相等了,說明沒有線程在幫助他們擴容了。也就是說,擴容結束了。
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    // 不相等,說明沒結束,當前線程結束方法。
                    return;
                // 如果相等,擴容結束了,更新 finising 變量
                finishing = advance = true;
                // 再次循環檢查一下整張表
                i = n; // recheck before commit
            }
        }
        // 如果沒有完成任務,且 i 對應的槽位是空,嘗試 CAS 插入佔位符,讓 putVal 方法的線程感知。
        else if ((f = tabAt(tab, i)) == null)
            advance = casTabAt(tab, i, null, fwd);
        // 如果 i 對應的槽位不是空,且有了佔位符,那麼該線程跳過這個槽位,處理下一個槽位。
        else if ((fh = f.hash) == MOVED)
            advance = true; // already processed
        else {
            // 如果以上都是不是,說明這個槽位有一個實際的值。開始同步處理這個桶。
            // 到這裏,都還沒有對桶內數據進行轉移,只是計算了下標和處理區間,然後一些完成狀態判斷。同時,如果對應下標內沒有數據或已經被佔位了,就跳過了。
            // 下面的處理過程和HashMap基本一樣
            synchronized (f) {
                // 再次判斷當前節點是否發生了改變
                if (tabAt(tab, i) == f) {
                    // ln=lowNode=低位桶,hn=highNode=高位桶
                    Node<K,V> ln, hn;
                    // 當前是鏈表結構
                    if (fh >= 0) {
                        // 當前節點hash和老長度進行與運算
                        int runBit = fh & n;
                        Node<K,V> lastRun = f;
                        // 從當前節點的後繼開始遍歷
                        for (Node<K,V> p = f.next; p != null; p = p.next) {
                            // 對每個節點的hash同長度進行按位與操作
                            int b = p.hash & n;
                            // 如果節點的 hash 值和首節點的 hash 值按位與結果不同
                            if (b != runBit) {
                                // 更新 runBit,用於下面判斷 lastRun 該賦值給 ln 還是 hn。
                                runBit = b;
                                // 這個 lastRun 保證後面的節點與自己的按位與值相同,避免後面沒有必要的循環
                                lastRun = p;
                            }
                        }
                        if (runBit == 0) {
                            // 如果最後更新的 runBit 是 0 ,設置低位節點
                            ln = lastRun;
                            hn = null;
                        }
                        else {
                            // 否則設置高位節點
                            hn = lastRun;
                            ln = null;
                        }
                        // 從頭開始循環,目的是生成兩個鏈表,lastRun 作爲停止條件,這樣做爲了避免不必要的循環(lastRun 後面都是相同的hash按位與結果)
                        for (Node<K,V> p = f; p != lastRun; p = p.next) {
                            int ph = p.hash; K pk = p.key; V pv = p.val;
                            // 依然根據是否爲0作爲區分條件
                            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);
                        // 在老數組i的位置的鏈表設置成佔位符
                        setTabAt(tab, i, fwd);
                        // 繼續向後
                        advance = true;
                    }
                    // 樹結構
                    else if (f instanceof TreeBin) {
                        // 當前位置的頭節點,只不過是TreeNode
                        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);
                            // 當前節點的hash和老長度做按位與操作,爲0放在低位
                            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;
                            }
                        }
                        // 低位達到臨界值,低位退化
                        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;
                        // 在新的數組i的位置上設置低位樹
                        setTabAt(nextTab, i, ln);
                        // 在新的數組i+n的位置上設置高位鏈表
                        setTabAt(nextTab, i + n, hn);
                        // 老數組i的位置上設置佔位符
                        setTabAt(tab, i, fwd);
                        // 繼續向後
                        advance = true;
                    }
                }
            }
        }
    }
}

總體來說分爲兩部分:

1. 擴容前的準備和相關狀態的檢查

①:初始化用於存儲擴容後數據的nextTable

②:分配一個桶給當前線程;判斷是否擴容結束,擴容結束更新table和sizeCtl變量;判斷當前桶是不是被佔用了,被佔用則跳過這個桶;

2. 加鎖擴容

①:判斷節點類型

②:如果是鏈表,從頭結點開始遍歷鏈表,通過當前節點老長度按位與操作生成一個runBit,每次遇到與前一個runBit不同的節點,則更新runBit和lastRun(當前與前面runBit不同的節點),直到遍歷結束;

③:根據runBit是否爲0,把lastRun節點賦給低位鏈表或者高位鏈表;

④:再次遍歷鏈表,分割出兩部分鏈表:以lastRun節點爲停止遍歷條件,根據每個Node的hash和老長度的按位與結果是否爲0,把Node劃分到低位鏈表和高位鏈表中。最後把低位鏈表和高位鏈表放到新數組i和i+n的位置上,老數組i的位置上設置佔位符。繼續處理其它剩餘的桶。

⑤:處理樹形結構,邏輯和鏈表一樣,只不過多了個判斷是否退化成鏈表的邏輯。

擴容過程我畫了個圖

 

 

 最後設置新位置

 

 

 

 

 

 未完待續

 

ConcurrentSkipListMap

--

 

TreeMap

--

.

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