併發程式設計之ConcurrentHashMap jdk1.7和1.8原始碼剖析
目錄
3.1.2 ConcurrentHashMap的主要資料結構
3.1.7 containsKey和containsValue
一、背景:
執行緒不安全的HashMap
因為多執行緒環境下,使用Hashmap進行put操作會引起死迴圈,導致CPU利用率接近100%,所以在併發情況下不能使用HashMap。
效率低下的HashTable容器
HashTable容器使用synchronized來保證執行緒安全,但線上程競爭激烈的情況下HashTable的效率非常低下。因為當一個執行緒訪問HashTable的同步方法時,其他執行緒訪問HashTable的同步方法時,可能會進入阻塞或輪詢狀態。如執行緒1使用put進行新增元素,執行緒2不但不能使用put方法新增元素,並且也不能使用get方法來獲取元素,所以競爭越激烈效率越低。
二、應用場景
CHM適用於讀者數量超過寫者時,當寫者數量大於等於讀者時,CHM的效能是低於Hashtable和synchronized Map的。這是因為當鎖住了整個Map時,讀操作要等待對同一部分執行寫操作的執行緒結束。CHM適用於做cache,在程式啟動時初始化,之後可以被多個請求執行緒訪問。正如Javadoc說明的那樣,CHM是HashTable一個很好的替代,但要記住,CHM的比HashTable的同步性稍弱。
三、原始碼分析:
3.1 jdk1.7的原始碼
3.1.1鎖分段技術
HashTable容器在競爭激烈的併發環境下表現出效率低下的原因,是因為所有訪問HashTable的執行緒都必須競爭同一把鎖,那假如容器裡有多把鎖,每一把鎖用於鎖容器其中一部分資料,那麼當多執行緒訪問容器裡不同資料段的資料時,執行緒間就不會存在鎖競爭,從而可以有效的提高併發訪問效率,這就是ConcurrentHashMap所使用的鎖分段技術,首先將資料分成一段一段的儲存,然後給每一段資料配一把鎖,當一個執行緒佔用鎖訪問其中一個段資料的時候,其他段的資料也能被其他執行緒訪問。有些方法需要跨段,比如size()和containsValue(),它們可能需要鎖定整個表而而不僅僅是某個段,這需要按順序鎖定所有段,操作完畢後,又按順序釋放所有段的鎖。這裡“按順序”是很重要的,否則極有可能出現死鎖,在ConcurrentHashMap內部,段陣列是final的,並且其成員變數實際上也是final的,但是,僅僅是將陣列宣告為final的並不保證陣列成員也是final的,這需要實現上的保證。這可以確保不會出現死鎖,因為獲得鎖的順序是固定的。
ConcurrentHashMap是由Segment陣列結構和HashEntry陣列結構組成。Segment是一種可重入鎖ReentrantLock,在ConcurrentHashMap裡扮演鎖的角色,HashEntry則用於儲存鍵值對資料。一個ConcurrentHashMap裡包含一個Segment陣列,Segment的結構和HashMap類似,是一種陣列和連結串列結構, 一個Segment裡包含一個HashEntry陣列,每個HashEntry是一個連結串列結構的元素, 每個Segment守護者一個HashEntry陣列裡的元素,當對HashEntry陣列的資料進行修改時,必須首先獲得它對應的Segment鎖。
3.1.2 ConcurrentHashMap的主要資料結構
ConcurrentHashMap(1.7及之前)中主要實體類就是三個:ConcurrentHashMap(整個Hash表),Segment(桶),HashEntry(節點),對應上面的圖可以看出之間的關係
ConcurrentHashMap的成員變數
//初始的容量
static final int DEFAULT_INITIAL_CAPACITY = 16;
//初始的載入因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//初始的併發等級(下面會敘述作用)
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//最小的segment數量
static final int MIN_SEGMENT_TABLE_CAPACITY = 2;
//最大的segment數量
static final int MAX_SEGMENTS = 1 << 16;
static final int RETRIES_BEFORE_LOCK = 2;
/**
* Segment的結構和HashMap類似,是一種陣列和連結串列結構, 一個Segment裡包含一個HashEntry陣列
* 每個Segment相當於一個子Hash表
*/
final Segment<K,V>[] segments;
/**
* segmentMask和segmentShift主要是為了定位段
*/
final int segmentMask;
final int segmentShift;
Segment的結構
static final class Segment<K,V> extends ReentrantLock implements Serializable {
//volatile,這使得能夠讀取到最新的 table值而不需要同步
transient volatile HashEntry<K,V>[] table;
//count用來統計該段資料的個數
transient int count;
//modCount統計段結構改變的次數,主要是為了檢測對多個段進行遍歷過程中某個段是否發生改變
transient int modCount;
//threashold用來表示需要進行rehash的界限值
transient int threshold;
//loadFactor表示負載因子。
final float loadFactor;
Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
this.loadFactor = lf;
this.threshold = threshold;
this.table = tab;
}
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
//略
}
private void rehash(HashEntry<K,V> node) {
//略
}
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
//略
}
private void scanAndLock(Object key, int hash) {
//略
}
final V remove(Object key, int hash, Object value) {
//略
}
final boolean replace(K key, int hash, V oldValue, V newValue) {
//略
}
final V replace(K key, int hash, V value) {
//略
}
final void clear() {
//略
}
}
HashEntry的結構
static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
}
3.1.3 hash槽的的個數
為了加快定位段以及段中hash槽的速度,每個段hash槽的的個數都是2^n,這使得通過位運算就可以定位段和段中hash槽的位置。當併發級別為預設值16時,也就是段的個數,hash值的高4位決定分配在哪個段中。
3.1.4 定位操作:
private Segment<K,V> segmentForHash(int h) {
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
return (Segment<K,V>) UNSAFE.getObjectVolatile(segments, u);
}
既然ConcurrentHashMap使用分段鎖Segment來保護不同段的資料,那麼在插入和獲取元素的時候,必須先通過雜湊演算法定位到Segment。可以看到ConcurrentHashMap會首先使用Wang/Jenkins hash的變種演算法對元素的hashCode進行一次再雜湊。
再雜湊,其目的是為了減少雜湊衝突,使元素能夠均勻的分佈在不同的Segment上,從而提高容器的存取效率。假如雜湊的質量差到極點,那麼所有的元素都在一個Segment中,不僅存取元素緩慢,分段鎖也會失去意義。
預設情況下segmentShift為28,segmentMask為15,再雜湊後的數最大是32位二進位制資料,向右無符號移動28位,意思是讓高4位參與到hash運算中, (hash >>> segmentShift) & segmentMask的運算結果分別是4,15,7和8,可以看到hash值沒有發生衝突。
3.1.5remove(key)操作
/**
* 先定位到段,然後委託給段的remove操作。當多個刪除操作併發進行時,
* 只要它們所在的段不相同,它們就可以同時進行。
*/
public V remove(Object key) {
int hash = hash(key);
Segment<K,V> s = segmentForHash(hash);
return s == null ? null : s.remove(key, hash, null);
}
下面是Segment的remove方法實現:
final V remove(Object key, int hash, Object value) {
if (!tryLock())
scanAndLock(key, hash);
V oldValue = null;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> e = entryAt(tab, index);
//pred用來記錄待每次迴圈的前一個節點
HashEntry<K,V> pred = null;
while (e != null) {
K k;
HashEntry<K,V> next = e.next;
//當找到了待刪除及節點的位置
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
V v = e.value;
if (value == null || value == v || value.equals(v)) {
//如果待刪除節點的前節點為null,即待刪除節點時鏈頭節點,此時把第2
//個結點設為頭結點
if (pred == null)
setEntryAt(tab, index, next);
else
//如果有前節點,則待刪除節點的前節點的next指向待刪除節點的的下
//一個節點,刪除成功
pred.setNext(next);
++modCount;
--count;
oldValue = v;
}
break;
}
pred = e;
e = next;
}
} finally {
unlock();
}
return oldValue;
}
3.1.5 get操作
public V get(Object key) {
Segment<K,V> s;
HashEntry<K,V>[] tab;
int h = hash(key);
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&(tab = s.table) != null) {
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);e != null; e = e.next) {
K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
get 邏輯比較簡單:只需要將 Key 通過 Hash 之後定位到具體的 Segment ,再通過一次 Hash 定位到具體的元素上。
由於 HashEntry 中的 value 屬性是用 volatile 關鍵詞修飾的,保證了記憶體可見性,所以每次獲取時都是最新值。
ConcurrentHashMap 的 get 方法是非常高效的,因為整個過程都不需要加鎖。
3.1.6 put操作
put操作是委託給段的put方法。下面是段的put方法:
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 1. 將當前 Segment 中的 table 通過 key 的 hashcode 定位到 HashEntry
HashEntry<K,V> node = tryLock() ? null :
// 嘗試獲取鎖,如果獲取失敗肯定就有其他執行緒存在競爭,則利用scanAndLockForPut()
//自旋獲取鎖。
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {
K k;
// 2. 遍歷該 HashEntry,如果不為空則判斷傳入的 key 和當前遍歷的 key
// 是否相等,相等則覆蓋舊的 value
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
// 3. 為空則需要新建一個 HashEntry 並加入到 Segment 中,同時會先判斷是否需要擴容
else {
if (node != null)
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
由於put方法裡需要對共享變數進行寫入操作,所以為了執行緒安全,在操作共享變數時必須得加鎖。Put方法首先定位到Segment,然後在Segment裡進行插入操作。插入操作需要經歷兩個步驟,第一步判斷是否需要對Segment裡的HashEntry陣列進行擴容,第二步定位新增元素的位置然後放在HashEntry陣列裡。
- 是否需要擴容。在插入元素前會先判斷Segment裡的HashEntry陣列是否超過容量(threshold),如果超過閥值,陣列進行擴容。值得一提的是,Segment的擴容判斷比HashMap更恰當,因為HashMap是在插入元素後判斷元素是否已經到達容量的,如果到達了就進行擴容,但是很有可能擴容之後沒有新元素插入,這時HashMap就進行了一次無效的擴容。
- 如何擴容。擴容的時候首先會建立一個兩倍於原容量的陣列,然後將原陣列裡的元素進行再hash後插入到新的陣列裡。為了高效ConcurrentHashMap不會對整個容器進行擴容,而只對某個segment進行擴容。
3.1.7 containsKey和containsValue
public boolean containsKey(Object key) {
Segment<K,V> s; // same as get() except no need for volatile value read
HashEntry<K,V>[] tab;
int h = hash(key);
//根據key定位到segment,遍歷HashEntry判斷是否具有key
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) {
K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return true;
}
}
return false;
}
public boolean containsValue(Object value) {
// Same idea as size()
if (value == null)
throw new NullPointerException();
final Segment<K,V>[] segments = this.segments;
boolean found = false;
long last = 0;
int retries = -1;
try {
outer: for (;;) {
//如果自旋的次數超過RETRIES_BEFORE_LOCK,強制堆所有segments加鎖
if (retries++ == RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
ensureSegment(j).lock(); // force creation
}
long hashSum = 0L;
int sum = 0;
//遍歷Segment
for (int j = 0; j < segments.length; ++j) {
HashEntry<K,V>[] tab;
Segment<K,V> seg = segmentAt(segments, j);
if (seg != null && (tab = seg.table) != null) {
for (int i = 0 ; i < tab.length; i++) {
HashEntry<K,V> e;
//遍歷HashEntry
for (e = entryAt(tab, i); e != null; e = e.next) {
V v = e.value;
if (v != null && value.equals(v)) {
found = true;
break outer;
}
}
}
sum += seg.modCount;
}
}
if (retries > 0 && sum == last)
break;
last = sum;
}
} finally {
if (retries > RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
segmentAt(segments, j).unlock();
}
}
return found;
}
3.1.8 size()操作
如果我們要統計整個ConcurrentHashMap裡元素的大小,就必須統計所有Segment裡元素的大小後求和。Segment裡的全域性變數count是一個volatile變數,那麼在多執行緒場景下,我們是不是直接把所有Segment的count相加就可以得到整個ConcurrentHashMap大小了呢?不是的,雖然相加時可以獲取每個Segment的count的最新值,但是拿到之後可能累加前使用的count發生了變化,那麼統計結果就不準了。所以最安全的做法,是在統計size的時候把所有Segment的put,remove和clean方法全部鎖住,但是這種做法顯然非常低效。
因為在累加count操作過程中,之前累加過的count發生變化的機率非常小,所以ConcurrentHashMap的做法是先嚐試2(RETRIES_BEFORE_LOCK)次通過不鎖住Segment的方式來統計各個Segment大小,如果統計的過程中,容器的count發生了變化,則再採用加鎖的方式來統計所有Segment的大小。
那麼ConcurrentHashMap是如何判斷在統計的時候容器是否發生了變化呢?使用modCount變數,在put , remove和clean方法裡操作元素前都會將變數modCount進行加1,那麼在統計size前後比較modCount是否發生變化,從而得知容器的大小是否發生變化。
public int size() {
// Try a few times to get accurate count. On failure due to
// continuous async changes in table, resort to locking.
final Segment<K,V>[] segments = this.segments;
int size;
boolean overflow; // true if size overflows 32 bits
long sum; // sum of modCounts
long last = 0L; // previous sum
int retries = -1; // first iteration isn't retry
try {
for (;;) {
//嘗試2 次通過不鎖住Segment的方式來統計各個Segment大小
if (retries++ == RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
ensureSegment(j).lock(); // force creation
}
sum = 0L;
size = 0;
overflow = false;
for (int j = 0; j < segments.length; ++j) {
Segment<K,V> seg = segmentAt(segments, j);
if (seg != null) {
sum += seg.modCount;
int c = seg.count;
if (c < 0 || (size += c) < 0)
overflow = true;
}
}
//如果統計的過程中,容器的count發生了變化,則再採用加鎖的方式來統計所有Segment
//的大小。
if (sum == last)
break;
last = sum;
}
} finally {
if (retries > RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
segmentAt(segments, j).unlock();
}
}
return overflow ? Integer.MAX_VALUE : size;
}
詳細原始碼註釋可以看
3.2 jdk1.8的原始碼
3.2.1主要資料結構
1.8中放棄了Segment
臃腫的設計,取而代之的是採用Node
+ CAS
+ Synchronized
來保證併發安全進行實現,結構如下:
ConcurrentHashMap在put操作時,如果發現連結串列結構中的元素超過了TREEIFY_THRESHOLD(預設為8),則會把 連結串列轉換為紅黑樹,已便於提高查詢效率。程式碼如下:
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
ConcurrentHashMap,提供了許多內部類來進行輔助實現,如Node,TreeNode,TreeBin等等。下面我們就一起來看看ConcurrentHashMap幾個重要的內部類。
ConcurrentHashMap的成員變數
// 最大容量:2^30=1073741824
private static final int MAXIMUM_CAPACITY = 1 << 30;
// 預設初始值,必須是2的幕數
private static final int DEFAULT_CAPACITY = 16;
//
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
//
private static final float LOAD_FACTOR = 0.75f;
// 連結串列轉紅黑樹閥值,> 8 連結串列轉換為紅黑樹
static final int TREEIFY_THRESHOLD = 8;
// 樹轉連結串列閥值,小於等於6(tranfer時,lc、hc=0兩個計數器分別++記錄原bin、新binTreeNode數
//量,<=UNTREEIFY_THRESHOLD 則untreeify(lo))
static final int UNTREEIFY_THRESHOLD = 6;
//
static final int MIN_TREEIFY_CAPACITY = 64;
//
private static final int MIN_TRANSFER_STRIDE = 16;
//
private static int RESIZE_STAMP_BITS = 16;
// 2^15-1,help resize的最大執行緒數
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
// 32-16=16,sizeCtl中記錄size大小的偏移量
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
// forwarding nodes的hash值
static final int MOVED = -1;
// 樹根節點的hash值
static final int TREEBIN = -2;
// ReservationNode的hash值
static final int RESERVED = -3;
// 可用處理器數量
static final int NCPU = Runtime.getRuntime().availableProcessors();
// 用來存放Node節點資料的,預設為null,預設大小為16的陣列,每次擴容時大小總是2的冪次方;
transient volatile Node<K, V>[] table;
// 擴容時新生成的資料,陣列為table的兩倍;
private transient volatile Node<K, V>[] nextTable;
Node
在Node內部類中,其屬性value、next都是帶有volatile的。同時其對value的setter方法進行了特殊處理,不允許直接呼叫其setter方法來修改value的值。最後Node還提供了find方法來賦值map.get()。
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;
}
public final K getKey() {
return key;
}
public final V getValue() {
return val;
}
public final int hashCode() {
return key.hashCode() ^ val.hashCode();
}
public final String toString() {
return key + "=" + val;
}
public final V setValue(V value) {
throw new UnsupportedOperationException();
}
public final boolean equals(Object o) {
Object k, v, u;
Map.Entry<?, ?> e;
return ((o instanceof Map.Entry) && (k = (e = (Map.Entry<?, ?>) o).getKey()) != null
&& (v = e.getValue()) != null && (k == key || k.equals(key)) && (v == (u = val) || v.equals(u)));
}
/**
* Virtualized support for map.get(); overridden in subclasses.
*/
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;
}
}
TreeNode
我們在學習HashMap的時候就知道,HashMap的核心資料結構就是連結串列。在ConcurrentHashMap中就不一樣了,如果連結串列的資料過長是會轉換為紅黑樹來處理。當它並不是直接轉換,而是將這些連結串列的節點包裝成TreeNode放在TreeBin物件中,然後由TreeBin完成紅黑樹的轉換。所以TreeNode也必須是ConcurrentHashMap的一個核心類,其為樹節點類,定義如下:
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<K, V> find(int h, Object k) {
return findTreeNode(h, k, null);
}
/**
* 查詢hash為h,key為k的節點
*/
final TreeNode<K, V> findTreeNode(int h, Object k, Class<?> kc) {
if (k != null) {
TreeNode<K, V> p = this;
do {
int ph, dir;
K pk;
TreeNode<K, V> q;
TreeNode<K, V> pl = p.left, pr = p.right;
if ((ph = p.hash) > h)
p = pl;
else if (ph < h)
p = pr;
else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
return p;
else if (pl == null)
p = pr;
else if (pr == null)
p = pl;
else if ((kc != null || (kc = comparableClassFor(k)) != null)
&& (dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
else if ((q = pr.findTreeNode(h, k, kc)) != null)
return q;
else
p = pl;
} while (p != null);
}
return null;
}
}
原始碼展示TreeNode繼承Node,且提供了findTreeNode用來查詢查詢hash為h,key為k的節點。
TreeBin
該類並不負責key-value的鍵值對包裝,它用於在連結串列轉換為紅黑樹時包裝TreeNode節點,也就是說ConcurrentHashMap紅黑樹存放是TreeBin,不是TreeNode。該類封裝了一系列的方法,包括putTreeVal、lookRoot、UNlookRoot、remove、balanceInsetion、balanceDeletion。由於TreeBin的程式碼太長我們這裡只展示構造方法(構造方法就是構造紅黑樹的過程):
static final class TreeBin<K, V> extends Node<K, V> {
TreeNode<K, V> root;
volatile TreeNode<K, V> first;
volatile Thread waiter;
volatile int lockState;
// values for lockState
static final int WRITER = 1; // set while holding write lock
static final int WAITER = 2; // set when waiting for write lock
static final int READER = 4; // increment value for setting read lock
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;
}
//構造方法就是在構造一個紅黑樹的過程。
TreeBin(TreeNode<K, V> b) {
super(TREEBIN, null, null, null);
this.first = b;
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;
if (r == null) {
x.parent = null;
x.red = false;
r = x;
} else {
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;
}
}
}
}
this.root = r;
assert checkInvariants(root);
}
private final void lockRoot() {
//略
}
private final void unlockRoot() {
//略
}
private final void contendedLock() {
//略
}
final Node<K, V> find(int h, Object k) {
//略
}
final TreeNode<K, V> putTreeVal(int h, K k, V v) {
//略
}
final boolean removeTreeNode(TreeNode<K, V> p) {
//略
}
static <K, V> TreeNode<K, V> rotateLeft(TreeNode<K, V> root, TreeNode<K, V> p) {
//略
}
static <K, V> TreeNode<K, V> rotateRight(TreeNode<K, V> root, TreeNode<K, V> p) {
//略
}
static <K, V> TreeNode<K, V> balanceInsertion(TreeNode<K, V> root, TreeNode<K, V> x) {
//略
}
static <K, V> TreeNode<K, V> balanceDeletion(TreeNode<K, V> root, TreeNode<K, V> x) {
//略
}
static <K, V> boolean checkInvariants(TreeNode<K, V> t) {
//略
}
}
3.2.2紅黑樹
先看紅黑樹的基本概念:紅黑樹是一課特殊的平衡二叉樹,主要用它儲存有序的資料,提供高效的資料檢索,時間複雜度為O(lgn)。紅黑樹每個節點都有一個標識位表示顏色,紅色或黑色,具備五種特性:
每個節點非紅即黑
根節點為黑色
每個葉子節點為黑色。葉子節點為NIL節點,即空節點
如果一個節點為紅色,那麼它的子節點一定是黑色
從一個節點到該節點的子孫節點的所有路徑包含相同個數的黑色節點
對於紅黑樹而言,它主要包括三個步驟:左旋、右旋、著色。所有不符合上面五個特性的“紅黑樹”都可以通過這三個步驟調整為正規的紅黑樹。
3.2.2.1旋轉
當對紅黑樹進行插入和刪除操作時可能會破壞紅黑樹的特性。為了繼續保持紅黑樹的性質,則需要通過對紅黑樹進行旋轉和重新著色處理,其中旋轉包括左旋、右旋。
3.2.2.2左旋
左旋示意圖如下:
左旋處理過程比較簡單,將E的右孩子S調整為E的父節點、S節點的左孩子作為調整後E節點的右孩子。
3.2.2.3右旋
3.2.2.4紅黑樹插入節點
由於連結串列轉換為紅黑樹只有新增操作,加上篇幅有限所以這裡就只介紹紅黑樹的插入操作,關於紅黑樹的詳細情況,煩請各位Google。
在分析過程中,我們已下面一顆簡單的樹為案例,根節點G、有兩個子節點P、U,我們新增的節點為N
紅黑樹預設插入的節點為紅色,因為如果為黑色,則一定會破壞紅黑樹的規則5(從一個節點到該節點的子孫節點的所有路徑包含相同個數的黑色節點)。儘管預設的節點為紅色,插入之後也會導致紅黑樹失衡。紅黑樹插入操作導致其失衡的主要原因在於插入的當前節點與其父節點的顏色衝突導致(紅紅,違背規則4:如果一個節點為紅色,那麼它的子節點一定是黑色)。
要解決這類衝突就靠上面三個操作:左旋、右旋、重新著色。由於是紅紅衝突,那麼其祖父節點一定存在且為黑色,但是叔父節點U顏色不確定,根據叔父節點的顏色則可以做相應的調整。
3.2.2.4.1 叔父U節點是紅色
如果叔父節點為紅色,那麼處理過程則變得比較簡單了:更換G與P、U節點的顏色,下圖(一)。
當然這樣變色可能會導致另外一個問題了,就是父節點G與其父節點GG顏色衝突(上圖二),那麼這裡需要將G節點當做新增節點進行遞迴處理。
3.2.2.4.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是內側插入,之所以這樣區分是因為他們的處理方式是相對的。
3.2.2.4.2.1 外側插入
以N是P的右子節點、P是G的右子節點為例,這種情況的處理方式為:以P為支點進行左旋,然後交換P和G的顏色(P設定為黑色,G設定為紅色),如下:
左外側的情況(N是P的左子節點,P是G的左子節點)和上面的處理方式一樣,先右旋,然後重新著色。
3.2.2.4.2.2 內側插入
以N是P的左子節點,P是G的右子節點情況為例。內側插入的情況稍微複雜些,經過一次旋轉、著色是無法調整為紅黑樹的,處理方法如下:先進行一次右旋,再進行一次左旋,然後重新著色,即可完成調整。注意這裡兩次右旋都是以新增節點N為支點不是P。這裡將N節點的兩個NIL節點命名為X、Y。如下:
至於左內側則處理邏輯如下:先進行右旋,然後左旋,最後著色。
3.2.2.5紅黑樹的應用
TreeMap、TreeSet以及JDK1.8之後的HashMap底層都用到了紅黑樹。
紅黑樹這麼優秀,為何不直接使用紅黑樹得了?
我們知道紅黑樹屬於(自)平衡二叉樹,但是為了保持“平衡”是需要付出代價的,紅黑樹在插入新資料後可能需要通過左旋,右旋、變色這些操作來保持平衡,這費事啊。你說說我們引入紅黑樹就是為了查詢資料快,如果連結串列長度很短的話,根本不需要引入紅黑樹的,你引入之後還要付出代價維持它的平衡。但是連結串列過長就不一樣了。至於為什麼選 8 這個值呢?通過概率統計所得,這個值是綜合查詢成本和新增元素成本得出的最好的一個值。
3.2.3 put方法
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null)
throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K, V>[] tab = table;;) {
Node<K, V> f;
int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//1、如果相應位置的Node還未初始化,則通過CAS插入相應的資料;
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)
tab = helpTransfer(tab, f);
else {
//2、如果相應位置的Node不為空,且當前該節點不處於移動狀態,
//則對該節點加synchronized鎖,如果該節點的hash不小於0,則遍歷連結串列更新節點或插入新節點;
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
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<K, V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K, V>(hash, key, value, null);
break;
}
}
//3.如果該節點是TreeBin型別的節點,說明是紅黑樹結構,則通過putTreeVal方法往紅黑樹中插入節點;
} 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;
}
}
}
}
//4、如果binCount不為0,說明put操作對資料產生了影響,如果當前連結串列的個數達到8個,
//則通過treeifyBin方法轉化為紅黑樹,如果oldVal不為空,
//說明是一次更新操作,沒有對元素個數產生影響,則直接返回舊值;
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//5、如果插入的是一個新節點,則執行addCount()方法嘗試更新元素個數baseCount;
addCount(1L, binCount);
return null;
}
詳細原始碼註釋可以看
四、參考文章
相關文章
- 併發程式設計之 ConcurrentHashMap(JDK 1.8) putVal 原始碼分析程式設計HashMapJDK原始碼
- HashMap jdk1.7和1.8原始碼剖析HashMapJDK原始碼
- 併發程式設計之 LinkedBolckingQueue 原始碼剖析程式設計原始碼
- 併發程式設計之 ThreadLocal 原始碼剖析程式設計thread原始碼
- 併發程式設計之 ConcurrentLinkedQueue 原始碼剖析程式設計原始碼
- ConcurrentHashMap基於JDK1.8原始碼剖析HashMapJDK原始碼
- hashmap和concurrenthashmap原始碼分析(1.7/1.8)HashMap原始碼
- ConcurrentHashMap (jdk1.7)原始碼學習HashMapJDK原始碼
- 併發程式設計之 原始碼剖析 執行緒池 實現原理程式設計原始碼執行緒
- Java併發——ConcurrentHashMap(JDK 1.8)JavaHashMapJDK
- 多執行緒高併發程式設計(10) -- ConcurrentHashMap原始碼分析執行緒程式設計HashMap原始碼
- Java併發包原始碼學習系列:JDK1.8的ConcurrentHashMap原始碼解析Java原始碼JDKHashMap
- 併發容器之ConcurrentHashMap(JDK 1.8版本)HashMapJDK
- 併發程式設計之 Condition 原始碼分析程式設計原始碼
- 併發程式設計之 CyclicBarrier 原始碼分析程式設計原始碼
- 併發程式設計之 CountDown 原始碼分析程式設計原始碼
- 併發程式設計之 Exchanger 原始碼分析程式設計原始碼
- 併發程式設計之:AQS原始碼解析程式設計AQS原始碼
- 併發程式設計之 wait notify 方法剖析程式設計AI
- 併發程式設計之——寫鎖原始碼分析程式設計原始碼
- 併發程式設計之 SynchronousQueue 核心原始碼分析程式設計原始碼
- ConcurrentHashMap原始碼刨析(基於jdk1.7)HashMap原始碼JDK
- 併發程式設計 —— ConcurrentHashMap size 方法原理分析程式設計HashMap
- JDK1.8 ConcurrentHashMap原始碼閱讀JDKHashMap原始碼
- 併發程式設計—— FutureTask 原始碼分析程式設計原始碼
- 併發程式設計 —— Timer 原始碼分析程式設計原始碼
- 原始碼分析–ConcurrentHashMap與HashTable(JDK1.8)原始碼HashMapJDK
- Java併發程式設計:深入剖析ThreadLocalJava程式設計thread
- JUC併發程式設計基石AQS原始碼之結構篇程式設計AQS原始碼
- 元件 popup 設計和原始碼剖析元件原始碼
- 併發程式設計之Wait和Notify程式設計AI
- java 併發程式設計-AQS原始碼分析Java程式設計AQS原始碼
- ConcurrentHashMap 併發之美HashMap
- 併發程式設計——ConcurrentHashMap#transfer() 擴容逐行分析程式設計HashMap
- 併發程式設計 —— 原始碼分析公平鎖和非公平鎖程式設計原始碼
- 跟我一起剖析 Java 併發原始碼之 UnsafeJava原始碼
- Java 併發程式設計(十五) -- Semaphore原始碼分析Java程式設計原始碼
- Java 併發程式設計(十四) -- CyclicBarrier原始碼分析Java程式設計原始碼