J.U.C 之 ConcurrentHashMap 紅黑樹轉換

ConcurrentHashMap的實現過程,其中有提到在put操做時,若是發現鏈表結構中的元素超過了TREEIFY_THRESHOLD(默認爲8),則會把鏈表轉換爲紅黑樹,已便於提升查詢效率。代碼以下:java

if (binCount >= TREEIFY_THRESHOLD)
    treeifyBin(tab, i);
複製代碼

紅黑樹

先看紅黑樹的基本概念:紅黑樹是一棵特殊的平衡二叉樹,主要用它存儲有序的數據,提供高效的數據檢索,時間複雜度爲O(lgn)。紅黑樹每一個節點都有一個標識位表示顏色,紅色或黑色,具有五種特性:編程

1. 每一個節點非紅即黑
2. 根節點爲黑色
3. 每一個葉子節點爲黑色。葉子節點爲NIL節點,即空節點
4. 若是一個節點爲紅色,那麼它的子節點必定是黑色
5. 從一個節點到該節點的子孫節點的全部路徑包含相同個數的黑色節點
複製代碼

這五個特性,它在維護紅黑樹時選的格外重要數組

紅黑樹結構圖以下:ide

對於紅黑樹而言,它主要包括三個步驟:左旋、右旋、着色。全部不符合上面五個特性的「紅黑樹」均可以經過這三個步驟調整爲正規的紅黑樹。性能

旋轉

當對紅黑樹進行插入和刪除操做時可能會破壞紅黑樹的特性。爲了繼續保持紅黑樹的性質,則須要經過對紅黑樹進行旋轉和從新着色處理,其中旋轉包括左旋、右旋。this

左旋

左旋示意圖以下:spa

左旋處理過程比較簡單,將E的右孩子S調整爲E的父節點、S節點的左孩子做爲調整後E節點的右孩子。3d

右旋

紅黑樹插入節點

因爲鏈表轉換爲紅黑樹只有添加操做,加上篇幅有限因此這裏就只介紹紅黑樹的插入操做,關於紅黑樹的詳細狀況,煩請各位Google。code

在分析過程當中,咱們如下面一顆簡單的樹爲案例,根節點G、有兩個子節點P、U,咱們新增的節點爲Ncdn

紅黑樹默認插入的節點爲紅色,由於若是爲黑色,則必定會破壞紅黑樹的規則5(從一個節點到該節點的子孫節點的全部路徑包含相同個數的黑色節點)。

儘管默認的節點爲紅色,插入以後也會致使紅黑樹失衡。紅黑樹插入操做致使其失衡的主要緣由在於插入的當前節點與其父節點的顏色衝突致使(紅紅,違背規則4:若是一個節點爲紅色,那麼它的子節點一 定是黑色)。

要解決這類衝突就靠上面三個操做:左旋、右旋、從新着色。因爲是紅紅衝突,那麼其祖父節點必定存在且爲黑色,可是叔父節點U顏色不肯定,根據叔父節點的顏色則能夠作相應的調整。

  1. 叔父U節點是紅色

若是叔父節點爲紅色,那麼處理過程則變得比較簡單了:更換G與P、U節點的顏色,下圖一。

固然這樣變色可能會致使另一個問題了,就是父節點G與其父節點GG顏色衝突(上圖二),那麼這裏須要將G節點當作新增節點進行遞歸處理。

  1. 叔父U節點爲黑叔

若是當前節點的叔父節點U爲黑色,則須要根據當前節點N與其父節點P的位置決定,分爲四種狀況:

N是P的右子節點、P是G的右子節點
N是P的左子節點,P是G的左子節點
N是P的左子節點,P是G的右子節點
N是P的右子節點,P是G的左子節點
複製代碼

狀況一、2稱之爲外側插入、狀況三、4是內側插入,之因此這樣區分是由於他們的處理方式是相對的。

外側插入

以N是P的右子節點、P是G的右子節點爲例,這種狀況的處理方式爲:以P爲支點進行左旋,而後交換P和G的顏色(P設置爲黑色,G設置爲紅色),以下:

左外側的狀況(N是P的左子節點,P是G的左子節點)和上面的處理方式同樣,先右旋,而後從新着色。

內側插入

以N是P的左子節點,P是G的右子節點狀況爲例。內側插入的狀況稍微複雜些,通過一次旋轉、着色是沒法調整爲紅黑樹的,處理方法以下:先進行一次右旋,再進行一次左旋,而後從新着色,便可完成調整。注意這裏兩次右旋都是以新增節點N爲支點不是P。這裏將N節點的兩個NIL節點命名爲X、L。以下:

至於左內側則處理邏輯以下:先進行右旋,而後左旋,最後着色。

ConcurrentHashMap 的treeifyBin過程

ConcurrentHashMap的鏈表轉換爲紅黑樹過程就是一個紅黑樹增長節點的過程。在put過程當中,若是發現鏈表結構中的元素超過了TREEIFY_THRESHOLD(默認爲8),則會把鏈表轉換爲紅黑樹:

if (binCount >= TREEIFY_THRESHOLD)
    treeifyBin(tab, i);
treeifyBin主要的功能就是把鏈表全部的節點Node轉換爲TreeNode節點,以下:

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) {
            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));
                }
            }
        }
    }
}
複製代碼

先判斷當前Node的數組長度是否小於MIN_TREEIFY_CAPACITY(64),若是小於則調用tryPresize擴容處理以緩解單個鏈表元素過大的性能問題。不然則將Node節點的鏈表轉換爲TreeNode的節點鏈表,構建完成以後調用setTabAt()構建紅黑樹。TreeNode繼承Node,以下:

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;
       }
	......
}
複製代碼

咱們如下面一個鏈表做爲案例,結合源代碼來分析ConcurrentHashMap建立紅黑樹的過程:

put 12

12做爲根節點,直接爲將紅編程黑便可,對應源碼:

next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
if (r == null) {
    x.parent = null;
    x.red = false;
    r = x;
}
複製代碼

(【注】:爲了方便起見,這裏省略NIL節點,後面也同樣)

put 1

此時根節點root不爲空,則插入節點時須要找到合適的插入位置,源碼以下:

K k = x.key;
int h = x.hash;
Class<?> kc = null;
for (TreeNode<K,V> p = r;;) {
    int dir, ph;
    K pk = p.key;
    if ((ph = p.hash) > h)
        dir = -1;
    else if (ph < h)
        dir = 1;
    else if ((kc == null &&
              (kc = comparableClassFor(k)) == null) ||
             (dir = compareComparables(kc, k, pk)) == 0)
        dir = tieBreakOrder(k, pk);
        TreeNode<K,V> xp = p;
    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;
    }
}
複製代碼

從上面能夠看到起處理邏輯以下:

  1. 計算節點的hash值 p。dir 表示爲往左移仍是往右移。x 表示要插入的節點,p 表示帶比較的節點。

  2. 從根節點出發,計算比較節點p的的hash值 ph ,若ph > h ,則dir = -1 ,表示左移,取p = p.left。若p == null則插入,若 p != null,則繼續比較,直到直到一個合適的位置,最後調用balanceInsertion()方法調整紅黑樹結構。ph < h,右移。

  3. 若是ph = h,則表示節點「衝突」(和HashMap衝突一致),那怎麼處理呢?首先調用comparableClassFor()方法判斷節點的key是否實現了Comparable接口,若是kc != null ,則經過compareComparables()方法經過compareTo()比較帶下,若是仍是返回 0,即dir == 0,則調用tieBreakOrder()方法來比較了。tieBreakOrder以下:

static int tieBreakOrder(Object a, Object b) {
    int d;
    if (a == null || b == null ||
        (d = a.getClass().getName().
         compareTo(b.getClass().getName())) == 0)
        d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
             -1 : 1);
    return d;
}
複製代碼

tieBreakOrder()方法最終仍是經過調用System.identityHashCode()方法來比較。

肯定插入位置後,插入,因爲插入的節點有可能會打破紅黑樹的結構,因此插入後調用balanceInsertion()方法來調整紅黑樹結構。

static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root, TreeNode<K,V> x) {
    x.red = true;       // 全部節點默認插入爲紅
    for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {

        // x.parent == null,爲跟節點,置黑便可
        if ((xp = x.parent) == null) {
            x.red = false;
            return x;
        }
        // x 父節點爲黑色,或者x 的祖父節點爲空,直接插入返回
        else if (!xp.red || (xpp = xp.parent) == null)
            return root;

        /* * x 的 父節點爲紅色 * --------------------- * x 的 父節點 爲 其祖父節點的左子節點 */
        if (xp == (xppl = xpp.left)) {
            /* * x的叔父節點存在,且爲紅色,顏色交換便可 * x的父節點、叔父節點變爲黑色,祖父節點變爲紅色 */
            if ((xppr = xpp.right) != null && xppr.red) {
                xppr.red = false;
                xp.red = false;
                xpp.red = true;
                x = xpp;
            }
            else {
                /* * x 爲 其父節點的右子節點,則爲內側插入 * 則先左旋,而後右旋 */
                if (x == xp.right) {
                    // 左旋
                    root = rotateLeft(root, x = xp);
                    // 左旋以後x則會變成xp的父節點
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }

                /** * 這裏有兩部分。 * 第一部分:x 本來就是其父節點的左子節點,則爲外側插入,右旋便可 * 第二部分:內側插入後,先進行左旋,而後右旋 */
                if (xp != null) {
                    xp.red = false;
                    if (xpp != null) {
                        xpp.red = true;
                        root = rotateRight(root, xpp);
                    }
                }
            }
        }

        /** * 與上相對應 */
        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);
                    }
                }
            }
        }
    }
}
複製代碼

回到節點1,其父節點爲黑色,即:

else if (!xp.red || (xpp = xp.parent) == null)
    return root;
複製代碼

直接插入:

put 9

9做爲1的右子節點插入,可是存在紅紅衝突,此時9的並無叔父節點。9的父節點1爲12的左子節點,9爲其父節點1的右子節點,因此處理邏輯是先左旋,而後右旋,對應代碼以下:

if (x == xp.right) {
    root = rotateLeft(root, x = xp);
    xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {
    xp.red = false;
    if (xpp != null) {
        xpp.red = true;
        root = rotateRight(root, xpp);
    }
}
複製代碼

圖例變化以下:

put 2

節點2 做爲1 的右子節點插入,紅紅衝突,切其叔父節點爲紅色,直接變色便可,:

if ((xppr = xpp.right) != null && xppr.red) {
    xppr.red = false;
    xp.red = false;
    xpp.red = true;
    x = xpp;
}
複製代碼

對應圖例爲:

put 0

節點0做爲1的左子節點插入,因爲其父節點爲黑色,不會插入後不會打破紅黑樹結構,直接插入便可:

put 11

節點11做爲12的左子節點,其父節點12爲黑色,和0同樣道理,直接插入:

put 7

節點7做爲2右子節點插入,紅紅衝突,其叔父節點0爲紅色,變色便可:

put 19

節點19做爲節點12的右子節點,直接插入:

至此,整個過程已經完成了,最終結果以下:

相關文章
相關標籤/搜索