死磕Java並:J.U.C之ConcurrentHashMap紅黑樹轉換分析
作者:chessy
來源:Java技術驛站
在【死磕Java併發】-----J.U.C之Java併發容器:ConcurrentHashMap一文中詳細闡述了ConcurrentHashMap的實現過程,其中有提到在put操作時,如果發現連結串列結構中的元素超過了TREEIFY_THRESHOLD(預設為8),則會把連結串列轉換為紅黑樹,已便於提高查詢效率。
程式碼如下:
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
下面博主將詳細分析整個過程,並用一個連結串列轉換為紅黑樹的過程為案例來分析。博文從如下幾個方法進行分析闡述:
紅黑樹
ConcurrentHashMap連結串列轉紅黑樹原始碼分析
連結串列轉紅黑樹案例
紅黑樹
先看紅黑樹的基本概念:紅黑樹是一課特殊的平衡二叉樹,主要用它儲存有序的資料,提供高效的資料檢索,時間複雜度為O(lgn)。紅黑樹每個節點都有一個標識位表示顏色,紅色或黑色,具備五種特性:
每個節點非紅即黑
根節點為黑色
每個葉子節點為黑色。葉子節點為NIL節點,即空節點
如果一個節點為紅色,那麼它的子節點一定是黑色
從一個節點到該節點的子孫節點的所有路徑包含相同個數的黑色節點
請牢記這五個特性,它在維護紅黑樹時選的格外重要
紅黑樹結構圖如下:
對於紅黑樹而言,它主要包括三個步驟:左旋、右旋、著色。所有不符合上面五個特性的“紅黑樹”都可以通過這三個步驟調整為正規的紅黑樹。
旋轉
當對紅黑樹進行插入和刪除操作時可能會破壞紅黑樹的特性。為了繼續保持紅黑樹的性質,則需要通過對紅黑樹進行旋轉和重新著色處理,其中旋轉包括左旋、右旋。
左旋
左旋示意圖如下:
左旋處理過程比較簡單,將E的右孩子S調整為E的父節點、S節點的左孩子作為調整後E節點的右孩子。
右旋
紅黑樹插入節點
由於連結串列轉換為紅黑樹只有新增操作,加上篇幅有限所以這裡就只介紹紅黑樹的插入操作,關於紅黑樹的詳細情況,煩請各位Google。
在分析過程中,我們已下面一顆簡單的樹為案例,根節點G、有兩個子節點P、U,我們新增的節點為N
紅黑樹預設插入的節點為紅色,因為如果為黑色,則一定會破壞紅黑樹的規則5(從一個節點到該節點的子孫節點的所有路徑包含相同個數的黑色節點)。儘管預設的節點為紅色,插入之後也會導致紅黑樹失衡。紅黑樹插入操作導致其失衡的主要原因在於插入的當前節點與其父節點的顏色衝突導致(紅紅,違背規則4:如果一個節點為紅色,那麼它的子節點一定是黑色)。
要解決這類衝突就靠上面三個操作:左旋、右旋、重新著色。由於是紅紅衝突,那麼其祖父節點一定存在且為黑色,但是叔父節點U顏色不確定,根據叔父節點的顏色則可以做相應的調整。
1 叔父U節點是紅色
如果叔父節點為紅色,那麼處理過程則變得比較簡單了:更換G與P、U節點的顏色,下圖(一)。
當然這樣變色可能會導致另外一個問題了,就是父節點G與其父節點GG顏色衝突(上圖二),那麼這裡需要將G節點當做新增節點進行遞迴處理。
2 叔父U節點為黑叔
如果當前節點的叔父節點U為黑色,則需要根據當前節點N與其父節點P的位置決定,分為四種情況:
N是P的右子節點、P是G的右子節點
N是P的左子節點,P是G的左子節點
N是P的左子節點,P是G的右子節點
N是P的右子節點,P是G的左子節點
情況1、2稱之為外側插入、情況3、4是內側插入,之所以這樣區分是因為他們的處理方式是相對的。
2.1 外側插入
以N是P的右子節點、P是G的右子節點為例,這種情況的處理方式為:以P為支點進行左旋,然後交換P和G的顏色(P設定為黑色,G設定為紅色),如下:
左外側的情況(N是P的左子節點,P是G的左子節點)和上面的處理方式一樣,先右旋,然後重新著色。
2.2 內側插入
以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節點,如下:
privatefinalvoid 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); elseif ((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 = newTreeNode<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, newTreeBin<K,V>(hd));
}
}
}
}
}
先判斷當前Node的陣列長度是否小於MIN_TREEIFY_CAPACITY(64),如果小於則呼叫tryPresize擴容處理以緩解單個連結串列元素過大的效能問題。否則則將Node節點的連結串列轉換為TreeNode的節點連結串列,構建完成之後呼叫setTabAt()構建紅黑樹。
TreeNode繼承Node,如下:
staticfinalclassTreeNode<K,V> extendsNode<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建立紅黑樹的過程:
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節點,後面也一樣)
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; elseif (ph < h)
dir = 1; elseif ((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;
}
}
從上面可以看到起處理邏輯如下:
計算節點的hash值 p。dir 表示為往左移還是往右移。x 表示要插入的節點,p 表示帶比較的節點。
從根節點出發,計算比較節點p的的hash值 ph ,若ph > h ,則dir = -1 ,表示左移,取p = p.left。若p == null則插入,若 p != null,則繼續比較,直到直到一個合適的位置,最後呼叫balanceInsertion()方法調整紅黑樹結構。ph < h,右移。
如果ph = h,則表示節點“衝突”(和HashMap衝突一致),那怎麼處理呢?首先呼叫comparableClassFor()方法判斷節點的key是否實現了Comparable介面,如果kc != null ,則通過compareComparables()方法通過compareTo()比較帶下,如果還是返回 0,即dir == 0,則呼叫tieBreakOrder()方法來比較了。
tieBreakOrder如下:
staticint 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 的祖父節點為空,直接插入返回
elseif (!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 為 其父節點的右子節點,則為內側插入
* 則先左旋,然後右旋