ConcurrentHashMap解析(JDK1.8)

weixin_34293059發表於2017-07-17

一 成員變數解析

1 transient volatile Node<K,V>[] table

預設為null,初始化發生在第一次插入操作,預設大小為16的陣列,用來儲存Node節點資料,擴容時大小總是2的冪次方。

2 private transient volatile Node<K,V>[] nextTable

預設為null,擴容時新生成的陣列,其大小為原陣列的兩倍。

3 Node

儲存key,value及key的hash值的資料結構。

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    volatile V val;
    volatile Node<K, V> next;

    Node(int hash, K key, V val, Node<K, V> next) {
        this.hash = hash;
        this.key = key;
        this.val = val;
        this.next = next;
    }
}

4 ForwardingNode

一個特殊的Node節點,hash值為-1,其中儲存nextTable的引用。

final class ForwardingNode<K,V> extends Node<K,V> {
    final Node<K,V>[] nextTable;
    ForwardingNode(Node<K,V>[] tab) {
        super(MOVED, null, null, null);
        this.nextTable = tab;
    }
}

5 static final int MOVED = -1

擴容節點的hash值

6 private transient volatile int sizeCtl

預設為0,用來控制table的初始化和擴容操作,具體應用在後續會體現出來。
-1 代表table正在初始化
-N 表示有N-1個執行緒正在進行擴容操作
其餘情況:
(1) 如果table未初始化,表示table需要初始化的大小。
(2) 如果table初始化完成,表示table的容量,預設是table大小的0.75倍,居然用這個公式算0.75(n - (n >>> 2))。

7 private transient volatile long baseCount

map中元素的長度

二 方法解析

1 例項初始化

例項化ConcurrentHashMap時帶引數時,會根據引數調整table的大小,假設引數為100,最終會調整成256,確保table的大小總是2的冪次方,演算法如下:

public ConcurrentHashMap(int initialCapacity) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException();
    int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
               MAXIMUM_CAPACITY :
               tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
    this.sizeCtl = cap;
}

private static final int tableSizeFor(int c) {
    int n = c - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

2 新增元素操作

(1)初始化tab

前面已經提到過,table初始化操作會延緩到第一次put行為。但是put是可以併發執行的,Doug Lea是如何實現table只初始化一次的?讓我們來看看原始碼的實現:

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
        //如果一個執行緒發現sizeCtl<0(即sizeCtl=-1),意味著另外的執行緒執行CAS操作成功,
        //當前執行緒只需要讓出cpu時間片
        if ((sc = sizeCtl) < 0)
            Thread.yield(); // lost initialization race; just spin
        // 執行Unsafe.compareAndSwapInt方法修改sizeCtl為-1,表示正在進行tab初始化
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                if ((tab = table) == null || tab.length == 0) {
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    // table初始化完成,表示table的容量,預設是table大小的0.75倍
                    sc = n - (n >>> 2);
                }
            } finally {
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

sizeCtl預設為0,如果ConcurrentHashMap例項化時有傳引數,sizeCtl會是一個2的冪次方的值。所以執行第一次put操作的執行緒會執行Unsafe.compareAndSwapInt方法修改sizeCtl為-1,有且只有一個執行緒能夠修改成功,其它執行緒通過Thread.yield()讓出CPU時間片等待table初始化完成。

(2)put()

假設table已經初始化完成,put操作採用CAS+synchronized實現併發插入或更新操作

final V putVal(K key, V value, boolean onlyIfAbsent) {
    // ConcurrentHashMap不允許key或者value為null,這點和HashMap不同
    if (key == null || value == null) throw new NullPointerException();
    // 對key的hash進行二次hash,減少雜湊衝突
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) { 
        Node<K,V> f; int n, i, fh;
        // 如果tab為空,則呼叫initTable()初始化tab
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        // tab[ (n - 1) & hash]為空節點,則直接建立一個新的節點
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // Doug Lea採用Unsafe.getObjectVolatile來獲取,也許有人質疑,直接table[index]不可以麼,
            // 為什麼要這麼複雜?在java記憶體模型中,我們已經知道每個執行緒都有一個工作記憶體,
            // 裡面儲存著table的副本,雖然table是volatile修飾的,但不能保證執行緒每次都拿到table中的最
            // 新元素,Unsafe.getObjectVolatile可以直接獲取指定記憶體的資料,
            // 保證了每次拿到資料都是最新的。
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        // 檢查table[i]的節點的hash是否等於MOVED,如果等於MOVED,則檢測到正在擴容,幫助其擴容
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else { // 節點f.hash不是MOVED
            V oldVal = null;
            // 鎖定,(hash值相同的連結串列的頭節點)
            synchronized (f) {
                // 避免多執行緒競爭,需要重新檢查
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        binCount = 1;
                        /*
                         *下面的程式碼就是先查詢連結串列中是否出現了此key,如果出現,則更新value,跳出迴圈
                         *否則將節點加入到連結串列末尾並跳出迴圈
                        */
                        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
                            */
                            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;
                        }
                    }
                }
            }
            // 如果連結串列長度已經達到臨界值8,就需要把連結串列轉換為樹結構
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                // 如果put是更新已有的key-value,那麼返回舊value
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    // 將當前ConcurrentHashMap的元素數量+1
    addCount(1L, binCount);
    return null;
}

(3)ConcurrentHashMap的三個原子操作

/**
 * 獲取tab在i位置上的節點
*/
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
    return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}

/**
 * 利用CAS方法設定tab在i位置上的節點
 * 將tab[i]和c作比較,相等則將tab[i]設定為v
 * 否則不做修改
*/
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                    Node<K,V> c, Node<K,V> v) {
    return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}

/**
 * 設定節點位置的值,僅在上鎖區被呼叫
*/
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
    U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}

put()方法的大致流程如下:
計算key兩次雜湊後的hash值
if(tab為空)
對tab進行初始化
計算tab索引index = hash & (tab.size-1)
else if(tab[index]為空)
直接為tab[index]建立新節點,退出迴圈
else if(tab[index].hash == MOVE,即tab[index]正在進行擴容)
呼叫helpTransfer(),幫助tab[index]擴容
else
用synchronized鎖定和key的hash值相同的連結串列的頭節點
if(tab[index].hash > 0)
通過key和key的hash找到要更新的節點,若沒有則在連結串列末尾新增新的node節點
else if(tab[index]是樹節點)
在樹種更新節點
增加baseCount的值

2 獲取元素

(1)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;
        }
        // 如果節點e的hash值小於0,說明該節點正在擴容,則呼叫節點的find()方法找尋
        else if (eh < 0)
            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;
}

(2)find

/**
 * Node類的find函式,通過遍歷以該節點為首的連結串列來找尋匹配的值
 */
Node<K, V> find(int h, Object k) {
    Node<K, V> e = this;
    if (k != null) {
        do {
            K ek;
            if (e.hash == h &&
                    ((ek = e.key) == k || (ek != null && k.equals(ek))))
                return e;
        } while ((e = e.next) != null);
    }
    return null;
}

3 擴容

(1)ForwardingNode:

一個特殊的Node節點,hash值為-1,其中儲存nextTable的引用。只有table發生擴容的時候,ForwardingNode才會發揮作用,作為一個佔位符放在table中表示當前節點為null或則已經被移動。

static final class ForwardingNode<K,V> extends Node<K,V> {
    final Node<K,V>[] nextTable;
    ForwardingNode(Node<K,V>[] tab) {
        super(MOVED, null, null, null);
        this.nextTable = tab;
    }
}

(2)擴容觸發時機

  • 如果新增節點之後,所在連結串列的元素個數達到了閾值 8 ,則會呼叫 treeifyBin 方法把連結串列轉換成紅黑樹,不過在結構轉換之前,會對陣列長度進行判斷。
    如果陣列長度n小於閾值 MIN_TREEIFY_CAPACITY ,預設是64,則會呼叫 tryPresize 方法把陣列長度擴大到原來的兩倍,並觸發 transfer 方法,重新調整節點的位置。
  • put方法時,如果當前節點的hash為-1,則呼叫helpTransfer方法來觸發擴容
  • addCount方法記錄元素個數後檢查是否需要進行擴容,當陣列元素個數達到閾值時,會觸發 transfer 方法,重新調整節點的位置。

(3)transfer

transfer方法程式碼略長,逐塊分析:

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range
    if (nextTab == null) {            // initiating
        try {
            @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 = n;
    }
    int nextn = nextTab.length;
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    boolean advance = true;
    boolean finishing = false; // to ensure sweep before committing nextTab
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;
        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 = nextTab;
                sizeCtl = (n << 1) - (n >>> 1);
                return;
            }
            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    return;
                finishing = advance = true;
                i = n; // recheck before commit
            }
        }
        else if ((f = tabAt(tab, i)) == null)
            advance = casTabAt(tab, i, null, fwd);
        else if ((fh = f.hash) == MOVED)
            advance = true; // already processed
        else {
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    Node<K,V> ln, hn;
                    if (fh >= 0) {
                        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);
                        }
                        setTabAt(nextTab, i, ln);
                        setTabAt(nextTab, i + n, hn);
                        setTabAt(tab, i, fwd);
                        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;
                            }
                        }
                        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;
                        setTabAt(nextTab, i, ln);
                        setTabAt(nextTab, i + n, hn);
                        setTabAt(tab, i, fwd);
                        advance = true;
                    }
                }
            }
        }
    }
}

transfer 方法實現了在併發的情況下,高效的從原始組數往新陣列中移動元素,假設擴容之前節點的分佈如下,這裡區分藍色節點和紅色節點,是為了後續更好的分析:


2184951-8566ed56aa3a1a9c.png

在上圖中,第14個槽位插入新節點之後,連結串列元素個數已經達到了8,且陣列長度為16,優先通過擴容來緩解連結串列過長的問題,實現如下:

  • 根據當前陣列長度n來新建一個長度為2n的nextTable
if (nextTab == null) {            // initiating
    try {
        @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 = n;
}
  • 初始化 ForwardingNode 節點,其中儲存了新陣列 nextTable 的引用,在處理完每個槽位的節點之後當做佔位節點,表示該槽位已經處理過了
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
  • 通過 for 自迴圈處理每個槽位中的連結串列元素,預設 advace 為真,通過CAS設定 transferIndex 屬性值,並初始化 i 和 bound 值, i 指當前處理的槽位序號, bound 指需要處理的槽位邊界,先處理槽位15的節點;
for (int i = 0, bound = 0;;) {
    Node<K,V> f; int fh;
    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;
        }
    }
  • 如果索引為15的節點tab[15]為空,則通過CAS插入ForwardingNode 節點表示該節點已經被其他執行緒處理過
else if ((f = tabAt(tab, i)) == null)
    advance = casTabAt(tab, i, null, fwd);
  • 如果索引為15的節點tab[15]的hash值等於-1,說明其他執行緒已經處理了該節點(該節點已經轉換完畢),則直接跳過,處理索引為14的節點
else if ((fh = f.hash) == MOVED)
    advance = true; // already processed
  • 處理槽位14的節點tab[14],是一個連結串列結構,先定義兩個變數節點 ln 和 hn ,分別儲存hash值的第X位為0和1的節點(X是由n來確定的,2^X=table.length),具體實現如下:
synchronized (f) {
    if (tabAt(tab, i) == f) {
        Node<K,V> ln, hn;
        if (fh >= 0) {
            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);
            }
            setTabAt(nextTab, i, ln);
            setTabAt(nextTab, i + n, hn);
            setTabAt(tab, i, fwd);
            advance = true;
        }

使用 fn&n 可以快速把連結串列中的元素區分成兩類,A類是hash值的第X位為0,B類是hash值的第X位為1,並通過 lastRun 記錄最後需要處理的節點,A類和B類節點可以分散到新陣列的槽位14和30中,在原陣列的槽位14中,藍色節點第X為0,紅色節點第X為1,把連結串列拉平顯示如下:


2184951-5e60c316353e8a8f.png
  • 通過遍歷連結串列,記錄 runBit 和 lastRun ,分別為1和節點6,所以設定 hn 為節點6, ln 為null;
  • 重新遍歷連結串列,以 lastRun 節點為終止條件,根據第X位的值分別構造ln連結串列和hn連結串列
    ln鏈:和原來連結串列相比,順序已經不一樣了


    2184951-00e946e7b274a8af.png

    hn鏈:通過CAS把ln連結串列設定到新陣列的i位置,hn連結串列設定到i+n的位置


    2184951-bcc2a0170ec52d9d.png

4 size計算

(1)addCount

1.8中使用一個volatile型別的變數baseCount記錄元素的個數,當插入新資料或則刪除資料時,會通過addCount()方法更新baseCount,實現如下:

@sun.misc.Contended static final class CounterCell {
    volatile long value;
    CounterCell(long x) { value = x; }
}

private final void addCount(long x, int check) {
    CounterCell[] as; long b, s;
    // 初始化時counterCells為空,在併發量很高時,如果存在兩個執行緒同時執行CAS修改baseCount值,
    // 則失敗的執行緒會繼續執行方法體中的邏輯,使用CounterCell記錄元素個數的變化
    if ((as = counterCells) != null ||
        !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
        // 使用CounterCell記錄元素個數的變化
        CounterCell a; long v; int m;
        boolean uncontended = true;
        // 如果CounterCell陣列counterCells為空,呼叫fullAddCount()方法進行初始化,
        // 並插入對應的記錄數,通過CAS設定cellsBusy欄位,只有設定成功的執行緒才能初始化CounterCell陣列
        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;
        s = sumCount();
    }
    if (check >= 0) {
        Node<K,V>[] tab, nt; int n, sc;
        while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
               (n = tab.length) < MAXIMUM_CAPACITY) {
            int rs = resizeStamp(n);
            if (sc < 0) {
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                    transferIndex <= 0)
                    break;
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                    transfer(tab, nt);
            }
            else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                         (rs << RESIZE_STAMP_SHIFT) + 2))
                transfer(tab, null);
            s = sumCount();
        }
    }
}

(2)size()

因為元素個數儲存baseCount中,部分元素的變化個數儲存在CounterCell陣列中,實現如下:

public int size() {
    long n = sumCount();
    return ((n < 0L) ? 0 :
            (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
            (int)n);
}

final long sumCount() {
    CounterCell[] as = counterCells; CounterCell a;
    long sum = baseCount;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    return sum;
}

通過累加baseCount和CounterCell陣列中的數量,即可得到元素的總個數

三 ConcurrentHashMap的紅黑樹實現分析

1 什麼是紅黑樹?

紅黑樹是一種特殊的二叉樹,主要用它儲存有序的資料,提供高效的資料檢索,新增、查詢、刪除等操作的時間複雜度為O(lgn),每個節點都有一個標識位表示顏色,紅色或黑色,有如下5種特性:
(1)每個節點要麼紅色,要麼是黑色
(2)根節點一定是黑色的
(3)每個空葉子節點必須是黑色的
(4)如果一個節點是紅色的,那麼它的兩個子節點必須是黑色的
(5)從一個節點到該節點的子孫節點的所有路徑包含相同個數的黑色節點

注意到性質4導致了路徑不能有兩個毗連的紅色節點就足夠了。最短的可能路徑都是黑色節點,最長的可能路徑有交替的紅色和黑色節點。因為根據性質5所有最長的路徑都有相同數目的黑色節點,這就表明了沒有路徑能多於任何其他路徑的兩倍長。
這些約束強制了紅黑樹的關鍵性質: 從根到葉子的最長的可能路徑不多於最短的可能路徑的兩倍長。

2 連結串列轉為紅黑樹

在1.8的實現中,當一個連結串列中的元素達到8個且tab長度大於等於64,會呼叫treeifyBin()方法把連結串列結構轉化成紅黑樹結構,實現如下:

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);
        else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
            // 對tab[index]加鎖,生成樹節點鏈
            synchronized (b) {
                if (tabAt(tab, index) == b) {
                    TreeNode<K,V> hd = null, tl = null;
                    // 遍歷tab[index]中的節點,將節電轉化為一條樹節點鏈
                    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;
                    }
                    // 將TreeNode連結串列轉化為TreeBin(即紅黑樹)
                    setTabAt(tab, index, new TreeBin<K,V>(hd));
                }
            }
        }
    }
}

static final class TreeNode<K,V> extends Node<K,V> {
    TreeNode<K,V> parent;  // red-black tree links
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // needed to unlink next upon deletion
    boolean red;

    TreeNode(int hash, K key, V val, Node<K,V> next,
             TreeNode<K,V> parent) {
        super(hash, key, val, next);
        this.parent = parent;
    }
}

從上述實現可以看出:並非一開始就建立紅黑樹結構,如果當前Node陣列長度小於閾值MIN_TREEIFY_CAPACITY,預設為64,先通過擴大陣列容量為原來的兩倍以緩解單個連結串列元素過大的效能問題。

static final class TreeBin<K,V> extends Node<K,V> {
    // 紅黑樹的根節點
    TreeNode<K,V> root;
    volatile TreeNode<K,V> first;

    TreeBin(TreeNode<K,V> b) {
        super(TREEBIN, null, null, null);
        this.first = b;
        // r儲存每次插入新節點後的樹的根節點
        TreeNode<K,V> r = null;
        for (TreeNode<K,V> x = b, next; x != null; x = next) {
            next = (TreeNode<K,V>)x.next;
            x.left = x.right = null;
            // 如果沒有根節點,說明是樹是空的,將新新增的節點設為根節點,設定為黑色(特性2)
            if (r == null) {
                x.parent = null;
                x.red = false;
                r = x;
            }
            else {
                // 如果根節點已存在,從根節點開始利用二分法比較帶插入節點和樹節點的hash值
                // 如果x節點的hash小於樹節點hash,則從樹節點的左子樹開始比較,直到左子樹為null時將x插入,
                // 然後balanceInsertion對紅黑樹進行調整
                // 如果x節點的hash大於樹節點hash,則從樹節點的右子樹開始比較,直到右子樹為null時將x插入,
                // 然後balanceInsertion對紅黑樹進行調整
                K k = x.key;
                int h = x.hash;
                Class<?> kc = null;
                for (TreeNode<K,V> p = r;;) {
                    int dir, ph;
                    K pk = p.key;
                    // x節點的hash小於樹節點hash,dir=-1
                    if ((ph = p.hash) > h)
                        dir = -1;
                    // x節點的hash大於樹節點hash,dir=1
                    else if (ph < h)
                        dir = 1;
                    // 如果x的hash值和p的hash值相等,首先判斷節點中的key物件的類是否實現了
                    // Comparable介面,如果實現Comparable介面,則呼叫compareTo方法比較兩者key的大小,
                    // 但是如果key物件沒有實現Comparable介面,或則compareTo方法返回了0,
                    // 則繼續呼叫tieBreakOrder方法計算dir值
                    else if ((kc == null &&
                              (kc = comparableClassFor(k)) == null) ||
                             (dir = compareComparables(kc, k, pk)) == 0)
                        dir = tieBreakOrder(k, pk);
                        TreeNode<K,V> xp = p;
                    // dir小於0,從左子樹開始比較;dir大於0,從右子樹開始比較
                    if ((p = (dir <= 0) ? p.left : p.right) == null) {
                        x.parent = xp;
                        if (dir <= 0)
                            xp.left = x;
                        else
                            xp.right = x;
                        // 插入後做平衡調整
                        r = balanceInsertion(r, x);
                        break;
                    }
                }
            }
        }
        // 最後設定root為節點全部插入後的根節點
        this.root = r;
        assert checkInvariants(root);
    }

    static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
                                                TreeNode<K,V> x) {
        // x是當前要處理的節點,xp為x的父節點,xpp為xp的父節點,xppl為xpp的左子節點,
        // xppr為xpp的右子節點
        x.red = true;
        for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
            // 如果xp為null,則將x設為根節點,返回x,迴圈退出
            if ((xp = x.parent) == null) {
                x.red = false;
                return x;
            }
            // 如果xp為黑色或xpp為null,返回root,迴圈退出
            else if (!xp.red || (xpp = xp.parent) == null)
                return root;
            // 如果xp是父節點的左子節點
            if (xp == (xppl = xpp.left)) {
                // 如果xp的兄弟節點(父節點的右子節點)是紅色,則將xp和其兄弟節點都變為黑色,
                // xpp變為紅色,並將當前節點設為xpp,繼續迴圈
                if ((xppr = xpp.right) != null && xppr.red) {
                    xppr.red = false;
                    xp.red = false;
                    xpp.red = true;
                    x = xpp;
                }
                // 如果xp的兄弟節點(父節點的右子節點)為null或顏色是黑色
                else {
                    // 如果x是xp的右子節點,則對xp進行左旋
                    if (x == xp.right) {
                        root = rotateLeft(root, x = xp);
                        xpp = (xp = x.parent) == null ? null : xp.parent;
                    }
                    // 如果xp不為null,則設xp為黑色
                    if (xp != null) {
                        xp.red = false;
                        // 如果xpp不為null,則設xpp為紅色,對xpp進行右旋
                        if (xpp != null) {
                            xpp.red = true;
                            root = rotateRight(root, xpp);
                        }
                    }
                }
            }
            // 如果xp是父節點的右子節點
            else {
                if (xppl != null && xppl.red) {
                    xppl.red = false;
                    xp.red = false;
                    xpp.red = true;
                    x = xpp;
                }
                else {
                    if (x == xp.left) {
                        root = rotateRight(root, x = xp);
                        xpp = (xp = x.parent) == null ? null : xp.parent;
                    }
                    if (xp != null) {
                        xp.red = false;
                        if (xpp != null) {
                            xpp.red = true;
                            root = rotateLeft(root, xpp);
                        }
                    }
                }
            }
        }
    }
}

由此我們可以看到將Node連結串列轉化為紅黑樹的大致步驟如下:

  • 首先呼叫treeifyBin(Node<K,V>[] tab, int index) 函式將Node連結串列依次轉化為TreeNode連結串列
  • 將TreeNode連結串列轉化為TreeBin物件,在轉化過程中建立紅黑樹,建立過程如下:
    (1)在TreeBin的建構函式中依次遍歷TreeNode中的節點並新增到紅黑樹中;
    (2)如果沒有根節點,說明是樹是空的,將新新增的節點設為根節點,設定為黑色,繼續執行(1);
    (3)否則,從根節點開始利用二分法比較帶插入節點和樹節點的hash值。如果x節點的hash小於樹節點hash,則從樹節點的左子樹開始比較,直到左子樹為null時將x插入,然後balanceInsertion對紅黑樹進行調整,繼續執行(1);
    (4)如果x節點的hash大於樹節點hash,則從樹節點的右子樹開始比較,直到右子樹為null時將x插入,然後balanceInsertion對紅黑樹進行調整,繼續執行(1);
    (5)如果x的hash值和p的hash值相等,首先判斷節點中的key物件的類是否實現了Comparable介面,如果實現Comparable介面,則呼叫compareTo方法比較兩者key的大小,但是如果key物件沒有實現Comparable介面,或則compareTo方法返回了0,則繼續呼叫tieBreakOrder方法計算dir值,得到dir值後將x插入,然後balanceInsertion對紅黑樹進行調整,繼續執行(1);
  • balanceInsertion函式會在插入新節點後檢查當前紅黑樹是否符合上述的5個性質,如果新增新節點打破了部分特性,則通過改變節點顏色或節點旋轉(左旋和右旋),具體分析見上面原始碼註釋

3 紅黑樹鎖的實現

當tab[index]為紅黑樹時,為了控制多執行緒併發訪問,紅黑樹自己實現了鎖。ConcurrentHashMap中紅黑樹的鎖是基於狀態位+CAS+LockSupport實現的

// 紅黑樹根節點
TreeNode<K,V> root;
// TreeNode連結串列的頭結點
volatile TreeNode<K,V> first;
volatile Thread waiter;
// 鎖狀態
volatile int lockState;

// 獲取了寫鎖的狀態
static final int WRITER = 1; 
// 等待寫鎖的狀態
static final int WAITER = 2;
static final int READER = 4; // increment value for setting read lock

/**
 * Acquires write lock for tree restructuring.
 */
private final void lockRoot() {
    // 如果當前狀態位0,則修改狀態為WRITER
    if (!U.compareAndSwapInt(this, LOCKSTATE, 0, WRITER))
        // 寫鎖獲取失敗,則競爭獲取
        contendedLock(); // offload to separate method
}

/**
 * Releases write lock for tree restructuring.
 */
private final void unlockRoot() {
    // 釋放鎖,將鎖狀態恢復0
    lockState = 0;
}

/**
 * Possibly blocks awaiting root lock.
 */
private final void contendedLock() {
    boolean waiting = false;
    // 自旋進行鎖競爭
    for (int s;;) {
        // 如果當前鎖狀態為WAITER(等待寫鎖)
        if (((s = lockState) & ~WAITER) == 0) {
            // 嘗試獲取寫鎖
            if (U.compareAndSwapInt(this, LOCKSTATE, s, WRITER)) {
                // 寫鎖獲取成功,將等待執行緒設為null,返回
                if (waiting)
                    waiter = null;
                return;
            }
        }
        // 如果當前狀態不是等待狀態
        else if ((s & WAITER) == 0) {
            // 設定當前狀態為等待狀態
            if (U.compareAndSwapInt(this, LOCKSTATE, s, s | WAITER)) {
                waiting = true;
                waiter = Thread.currentThread();
            }
        }
        else if (waiting)
            // 如果處於等待狀態,則阻塞該執行緒
            LockSupport.park(this);
    }
}

綜上我們可以看出獲取紅黑樹獲取寫鎖的流程:

  1. 通過CAS來設定當前縮狀態為寫鎖狀態(嘗試獲取寫鎖);
  2. 如果成功獲取寫鎖,則退出,否則繼續3;
  3. 進入contendedLock中自旋地獲取寫鎖,獲取成功則返回,否則執行緒阻塞,等待其他執行緒釋放寫鎖;

紅黑樹釋放寫鎖:

  1. 設定當前鎖狀態為0;

4 紅黑樹節點刪除

紅黑樹刪除節點的一共分為兩步:

  1. 在TreeNode連結串列中刪除節點x;
  2. 在紅黑樹中找到x的後繼節點s,然後將x和s交換位置以及顏色,之後呼叫balanceDeletion(TreeNode<K,V> root, TreeNode<K,V> x)調整紅黑樹
// p是待刪除節點
TreeNode<K,V> next = (TreeNode<K,V>)p.next;
TreeNode<K,V> pred = p.prev;  // unlink traversal pointers
TreeNode<K,V> r, rl;
if (pred == null)
    first = next;
else
    pred.next = next;
if (next != null)
    next.prev = pred;
if (first == null) {
    root = null;
    return true;
}
if ((r = root) == null || r.right == null || // too small
    (rl = r.left) == null || rl.left == null)
    return true;

上面的程式碼執行第一步操作,刪除TreeNode連結串列中x節點

lockRoot();
try {
    TreeNode<K,V> replacement;
    TreeNode<K,V> pl = p.left;
    TreeNode<K,V> pr = p.right;
    if (pl != null && pr != null) {
        TreeNode<K,V> s = pr, sl;
        while ((sl = s.left) != null) // find successor
            s = sl;
        boolean c = s.red; s.red = p.red; p.red = c; // swap colors
        TreeNode<K,V> sr = s.right;
        TreeNode<K,V> pp = p.parent;
        if (s == pr) { // p was s's direct parent
            p.parent = s;
            s.right = p;
        }
        else {
            TreeNode<K,V> sp = s.parent;
            if ((p.parent = sp) != null) {
                if (s == sp.left)
                    sp.left = p;
                else
                    sp.right = p;
            }
            if ((s.right = pr) != null)
                pr.parent = s;
        }
        p.left = null;
        if ((p.right = sr) != null)
            sr.parent = p;
        if ((s.left = pl) != null)
            pl.parent = s;
        if ((s.parent = pp) == null)
            r = s;
        else if (p == pp.left)
            pp.left = s;
        else
            pp.right = s;
        if (sr != null)
            replacement = sr;
        else
            replacement = p;
    }
    else if (pl != null)
        replacement = pl;
    else if (pr != null)
        replacement = pr;
    else
        replacement = p;
    if (replacement != p) {
        TreeNode<K,V> pp = replacement.parent = p.parent;
        if (pp == null)
            r = replacement;
        else if (p == pp.left)
            pp.left = replacement;
        else
            pp.right = replacement;
        p.left = p.right = p.parent = null;
    }

    root = (p.red) ? r : balanceDeletion(r, replacement);

    if (p == replacement) {  // detach pointers
        TreeNode<K,V> pp;
        if ((pp = p.parent) != null) {
            if (p == pp.left)
                pp.left = null;
            else if (p == pp.right)
                pp.right = null;
            p.parent = null;
        }
    }
} finally {
    unlockRoot();
}

上述程式碼執行第二步,在紅黑樹中找到x的後繼節點s,然後將x和s交換位置以及顏色,最後進行紅黑樹的平衡調整

相關文章