CHM1.7和1.8的更改
網上看了很多CHM的分析,可能都不是很讓我能有恍然大明白的感覺,畢竟1.7到1.8的巨大改動,肯定是有什麼更高深的算法或者設計在裏面。所以想自己去分析下。個人愚見,不喜歡請輕噴。
看了JDK1.8的CHM的代碼,洋洋灑灑6000+行,應該是jdk源碼最複雜的一個類了吧。頭疼,分析一下流程和核心API吧~
對比1.8和1.7的升級優化的區別如下:
- 1.7時候是CHM是多個Segment (默認應該是16個),Segment集成ReentrantLock來實現鎖定。所以鎖的粒度寬泛。每個segment對應一個數組+鏈表的組合。理論上支持最高16個線程併發寫入。
- 1.8時候CHM鎖是針對數組單個元素,鎖的粒度更加細微,衝突概率更低,效率更高。
- 1.8時候當鏈表過長的時候會升級爲紅黑樹。防止hash攻擊等降低查詢效率
- 1.7結構如圖
CHM1.8同時也維護了1.7的segment
CHM核心類和關鍵成員變量
Node
普通的數據節點,維護了hash, key , value, nextNode的屬性
TreeNode
紅黑樹的數據節點,維護了hash, key , value, nextNode的屬性
TreeBin
紅黑樹的根節點。沒實際數據意義,hash = -2
ForwardingNode
當map需要reszie的時候,放在鏈表頭的節點。沒實際數據意義,hash = -1
ReservationNode
當map是computeIfAbsent時候用於保留,hash = -3
sizeCtl
/**
* Table initialization and resizing control. When negative, the
* table is being initialized or resized: -1 for initialization,
* else -(1 + the number of active resizing threads). Otherwise,
* when table is null, holds the initial table size to use upon
* creation, or 0 for default. After initialization, holds the
* next element count value upon which to resize the table.
*/
private transient volatile int sizeCtl;
主要用途是用來標誌現在map的狀態的。
- 0 default
- -1 初始化
- -(1 + resize線程數) map正在resize,並且可以多線程resize
- >0 map下次resize的大小
CounterCell
/**
* A padded cell for distributing counts. Adapted from LongAdder
* and Striped64. See their internal docs for explanation.
*/
@sun.misc.Contended static final class CounterCell {
volatile long value;
CounterCell(long x) { value = x; }
}
精髓之一
用於計算totalSize,當併發過大的時候,如果通過CAS來進行同步,當然可以,但是效率很低。所以在計算totalSize的時候是通過baseCount和CountCell[]來計算的,運用了分流治之的思想。
CHM核心API分析
put
源碼如下:
/**
* Maps the specified key to the specified value in this table.
* Neither the key nor the value can be null.
*
* <p>The value can be retrieved by calling the {@code get} method
* with a key that is equal to the original key.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with {@code key}, or
* {@code null} if there was no mapping for {@code key}
* @throws NullPointerException if the specified key or value is null
*/
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
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) {
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 {
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;
}
}
}
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;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
雖然主函數只有不到100行-。-可是各個地方都透露着精髓~
初始化
第一個if:初始化
/**
* Initializes table, using the size recorded in sizeCtl.
*/
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
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;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
主要是通過對於sizeCtl 的判斷和cas進行初始化工作,哪個線程搶到了sizeCtl(這個變量是volatile的 ),哪個線程進行初始化。其中CAS是通過Unsafe類進行操作。
設置數組節點
第二個if是當前下標數組爲null,通過cas setValue
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
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);
}
這個地方有一個知識點,既然table是volatile的,爲什麼還要用Unsafe類進行cas呢?
因爲volatile只是對對象引用是線程可見的,對內部元素不是!
拓容
第三個if 判斷是否在resize
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
resize也是CHM的精髓之一
- 首先1.8的CHM支持多線程拓容
- 使用CAS來實現無鎖化的並行拓容,到底有多犀利!還得慢慢看doug Lea大神的思想
廢話不多說,先上源碼:
/**
* Helps transfer if a resize is in progress.
*/
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
int rs = resizeStamp(tab.length);
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
-
判斷是否需要幫助 —>看nextTable是否已經完成
- 完成則直接退出
- 沒完成則生成自己線程的Stamp進場幫忙resize
- 這裏用到了sizeCtl 多一個線程幫忙拓容,則通過CAS對sizeCtl + 1
- 具體看看如何多線程無鎖拓容的transfer函數
-
前提知識點:自己線程的stamp是什麼?(resizeStamp函數)
-
-
/** * The number of bits used for generation stamp in sizeCtl. * Must be at least 6 for 32bit arrays. */ private static int RESIZE_STAMP_BITS = 16; /** * The maximum number of threads that can help resize. * Must fit in 32 - RESIZE_STAMP_BITS bits. */ private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1; /** * The bit shift for recording size stamp in sizeCtl. */ private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS; /** * Returns the stamp bits for resizing a table of size n. * Must be negative when shifted left by RESIZE_STAMP_SHIFT. */ static final int resizeStamp(int n) { return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1)); }
-
首先拓容線程stamp和源table的長度有關~
比如n = 16 ; 二進制 —> 0000 0000 0000 0000 0000 0000 0001 0000
Interger.numberofLeadingZeros(16) = 27 (返回無符號數第一個不爲0數字前有多少個0)
27的二進制: 0000 0000 0000 0000 0000 0000 0001 1100
resizeStamp(16) 結果就是 —> 0000 0000 0000 0000 1000 0000 0001 1100
ok 到此爲止,線程的拓容戳已經創建好了~
-
-
-
前提知識點:sizeCtl的resize更新
-
第一次初始化resize值 ( 1 + resize的線程數)
-
else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2))
-
隨後增加線程幫助拓容 + 1
-
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
-
第一次sizeCtl更新爲(如果table長度是16)
- rs = 0000 0000 0000 0000 1000 0000 0001 1100
- (rs << RESIZE_STAMP_SHIFT) + 2)) = 1000 0000 0001 1100 0000 0000 0000 0010
- 10進製爲-2145648638
- 則sizeCtl則爲這個值-2145648638
-
再有線程進入幫忙拓容則在+1
- 則sizeCtl則+1
- 則sizeCtl = -2145648638 + 1 = -2145648637 = 1000 0000 0001 1100 0000 0000 0000 0011
- 所以不用注重10進制數值,僅注意2進制即可
-
高16位 低16位 拓容標記 並行拓容的線程數量
-
-
transfer函數
簡單來說,就是將原有table通過指針劃分成多個區間,然後各個線程負責自己的區間。每個區間的數組按照逆向遍歷的方式進行遷移,前已完成的數組下標的元素會標記爲ForwardingNode,表示該數組下標已經拓容完成。不多說先上源碼(以table.size = 16爲例):
/** Number of CPUS, to place bounds on some sizings */ static final int NCPU = Runtime.getRuntime().availableProcessors(); private static final int MIN_TRANSFER_STRIDE = 16; /** * Moves and/or copies the nodes in each bin to new table. See * above for explanation. */ private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) { int n = tab.length, stride; //計算每個線程負責區間的長度,通過當前機器的CPU和確定,如果小於16,則按照16來進行計算 if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE) stride = MIN_TRANSFER_STRIDE; // subdivide range //初始化,構建 << 1長度的table 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; //創建一個FowwardingNode,維護的是拓容後的數組。用來告知是否數組bucket已經拓容完成 ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab); //拓容是否推進,即是否拓容完了一個bucket,是否進行下一個 boolean advance = true; //節點是否已經拓容完畢 boolean finishing = false; // to ensure sweep before committing nextTab for (int i = 0, bound = 0;;) { //通過循環來處理bucket中的元素,通過CAS設置transferIndex,循環中i表示正在處理的bucket位置,bound表示需要處理的邊界,初始化transferIndex = 16. Node<K,V> f; int fh; while (advance) { int nextIndex, nextBound; //i = -1; // --i保證了正在處理的bucket向下的遍歷 if (--i >= bound || finishing) advance = false; //nextIndex = 16; else if ((nextIndex = transferIndex) <= 0) { i = -1; advance = false; } //transferIndex = 16; //nextBound = 0; else if (U.compareAndSwapInt (this, TRANSFERINDEX, nextIndex, nextBound = (nextIndex > stride ? nextIndex - stride : 0))) { //bound = 0; //i = 15; bound = nextBound; i = nextIndex - 1; advance = false; } } //經過分配後,當前線程的處理[0,15],transferIndex = 0; // 當完成了當前線程的任務 if (i < 0 || i >= n || i + n >= nextn) { int sc; // 拓容完成,更新成員變量 if (finishing) { nextTable = null; table = nextTab; sizeCtl = (n << 1) - (n >>> 1); return; } // 當前線程拓展完畢,則更新sizeCtl - 1 if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) { // 全部拓容線程更新完畢,則執行return if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT) return; // 更新當前線程標記量 finishing = advance = true; // i = 16 i = n; // recheck before commit } } // 如果當前老table的i = 15 位置爲空,則放一個fwd else if ((f = tabAt(tab, i)) == null) advance = casTabAt(tab, i, null, fwd); // 已經處理過的bucket else if ((fh = f.hash) == MOVED) advance = true; // already processed // 如果當前位置老table的i不爲Null,首先鎖定鏈表首節點 else { synchronized (f) { if (tabAt(tab, i) == f) { Node<K,V> ln, hn; if (fh >= 0) { int runBit = fh & n;//ln 表示低位, hn 表示高位;接下來這段代碼的作用 是把鏈表拆分成兩部分,0 在低位,1 在高位 Node<K,V> lastRun = f; //鏈表頭 for (Node<K,V> p = f.next; p != null; p = p.next) { // n = 16, 二進制是 1 0000, & 表示看p.hash的第5位是否爲1 int b = p.hash & n; if (b != runBit) { runBit = b; lastRun = p; } } // 開始一直不明白爲什麼需要加這個循環,直接區分高低位,形成兩個新的鏈表即可,後來仔細研究才明白,配合後面的循環,目的是爲了如果尾部的runbit都一致,那就沒必要在進行重新構造。 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); } // 鏈表插入到對應的位置,並且更新老table當前i的fwd-->已經遷移完成 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; } } } } } }
-
圖解:
-
計算區間大小 = 16;
-
初始化,如果原大小是16的話。
-
計算當前線程所處理的區間。如果是32拓展到64的話如圖:
-
遍歷各個線程負責的區間,根據i來處理對應的bucket.
-
原table第15個bucket是null
-
直接將老的table設置爲fwd.處理完成後如下:
-
繼續遍歷,一直到老table中一個不爲null的bucket,如圖:
-
遍歷鏈表計算runbit
-
構建新的高低鏈表
-
將新的高低鏈表插入到新的nextTable當中
-
更新老的table,把i的bucket設置爲fwd
-
當所有節點都fwd後,設置finishing,進行退出拓容操作。sizeCtl - 1,如果所有線程已經拓容完畢,則會判斷rs和sizeCtl的比較,重新設置sizeCtl~如下代碼:
-
-
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
}
}
總結:
充分利用了CAS和併發的效率,從而可以高效的進行拓容操作。
設置鏈表節點
最後是要遍歷鏈表或者數組進行SetValue
else {
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;
}
}
}
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;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
首先鎖住鏈表的head,防止其他線程進入,進入單線程模式~(這裏也就是1.7和1.8鎖的粒度不一致的地方了,1.8更高效,因爲鎖的粒度更低)
之後就是遍歷鏈表或者紅黑樹,和hashmap將數據放進去即可~
最後判斷一下數組長度,如果是大於紅黑樹閾值,就轉化爲紅黑樹數據結構。
AddCount
最後是重中之重 ,put了一個元素進去,該增加size了~
/**
* Adds to count, and if table is too small and not already
* resizing, initiates transfer. If already resizing, helps
* perform transfer if work is available. Rechecks occupancy
* after a transfer to see if another resize is already needed
* because resizings are lagging additions.
*
* @param x the count to add
* @param check if <0, don't check resize, if <= 1 only check if uncontended
*/
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
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();
}
}
}
流程圖如下:
第一步更新baseCount,通過CAS更新失敗,則說明存在併發,那麼走第二個if
第二步獲取隨機數,然後計算到底這個count應該加再哪個CounterCell數組中,然後通過cas添加。如果失敗,則說明存在競爭,進入fullAddCount方法
需要注意兩個地方:
- ThreadLocalRandom這個類繼承於Random,適合併發場景。這裏面隨機數是爲了尋找隨機的數組下標,減小衝突。而該類會比Randon減少衝突的次數。
- 這裏面和正常map尋找數組下標一樣,也是用&而不是用%。
第三步 fullAddCount方法我放棄了。簡要說下思路:
-
初始化CounterCell size = 2並且以<<1 拓容
-
本身方法是一個自旋
-
使用到了自旋鎖
/** * Spinlock (locked via CAS) used when resizing and/or creating CounterCells. */ private transient volatile int cellsBusy;
總結下爲什麼要這麼做?把這個addCount方法設計的這麼複雜。
- 不直接使用synchronized是爲了效率
- 不直接使用CAS,是害怕在高併發的時候,不停在CAS丟失了效率
- 使用baseCount + CounterCell[]的好處是在於
- 低併發的時候可以直接用baseCount解決問題
- 高併發的時候,可以通過ThreadLocalRandom和CounterCell[]進行很好的分流工作,有效的減少了無意義的鎖和CAS。類似於Nginx負載均衡的效果。
get
get相對來說比較簡單了直接尋找hash和key.equals上源碼:
/**
* Returns the value to which the specified key is mapped,
* or {@code null} if this map contains no mapping for the key.
*
* <p>More formally, if this map contains a mapping from a key
* {@code k} to a value {@code v} such that {@code key.equals(k)},
* then this method returns {@code v}; otherwise it returns
* {@code null}. (There can be at most one such mapping.)
*
* @throws NullPointerException if the specified key is null
*/
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
//正常尋找數組中的bucket位置
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
//hash相同的話,直接比較key
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
//bucket中的hash 是負數 -->可能在拓容,遍歷鏈表
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
//遍歷bucket的鏈表尋找元素
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
size
看過了put方法的addCount方法,其實這個就很簡單了,直接上源碼:
/**
* {@inheritDoc}
*/
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;
}
就是遍歷counterCell的值再累加baseCount即可~~
總結
- 在鎖很重的時候,如果可以用volatile和CAS來巧妙的避開
- 當併發大的時候,如果可以分而治之
- 架構和細節的優化同樣重要