1. 程式人生 > 程式設計 >J.U.C 之 ConcurrentHashMap 紅黑樹轉換

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

ConcurrentHashMap的實現過程,其中有提到在put操作時,如果發現連結串列結構中的元素超過了TREEIFY_THRESHOLD(預設為8),則會把連結串列轉換為紅黑樹,已便於提高查詢效率。程式碼如下:

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

紅黑樹

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

1. 每個節點非紅即黑
2. 根節點為黑色
3. 每個葉子節點為黑色。葉子節點為NIL節點,即空節點
4. 如果一個節點為紅色,那麼它的子節點一定是黑色
5. 從一個節點到該節點的子孫節點的所有路徑包含相同個數的黑色節點
複製程式碼

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

紅黑樹結構圖如下:

對於紅黑樹而言,它主要包括三個步驟:左旋、右旋、著色。所有不符合上面五個特性的“紅黑樹”都可以通過這三個步驟調整為正規的紅黑樹。

旋轉

當對紅黑樹進行插入和刪除操作時可能會破壞紅黑樹的特性。為了繼續保持紅黑樹的性質,則需要通過對紅黑樹進行旋轉和重新著色處理,其中旋轉包括左旋、右旋。

左旋

左旋示意圖如下:

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

右旋

紅黑樹插入節點

由於連結串列轉換為紅黑樹只有新增操作,加上篇幅有限所以這裡就只介紹紅黑樹的插入操作,關於紅黑樹的詳細情況,煩請各位Google。

在分析過程中,我們以下面一顆簡單的樹為案例,根節點G、有兩個子節點P、U,我們新增的節點為N

紅黑樹預設插入的節點為紅色,因為如果為黑色,則一定會破壞紅黑樹的規則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的左子節點
複製程式碼

情況1、2稱之為外側插入、情況3、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,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的右子節點,直接插入:

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