引言
這篇是介紹ConcurrentHashMap的第三篇,第一篇主要介紹了在jdk1.8中所用到的一些關鍵知識點,第二篇主要學習了ConcurrentHashMap的組織結構與線程安全的實現,同時介紹了幾個極其重要的內部類。這一篇主要是我學習領悟到的幾個核心方法,包括擴容,添加和查找。筆者目前整理的一些blog針對面試都是超高頻出現的。大家可以點擊鏈接:http://blog.csdn.net/u012403290
transfer方法(擴容方法)
再這之前,我大致描述一下擴容的過程:首先有且只能由一個線程構建一個nextTable,這個nextTable主要是擴容後的數組(容量已經擴大),然後把原table複製到nextTable中,這個過程可以多線程共同操作。但是一定要清楚,這個複製並不是簡單的把原table的數據直接移動到nextTable中,而是需要有一定的規律和算法操控的(不然怎麼把樹轉化爲鏈表呢)。
再這之前,先簡單說下複製的過程:
數組中(桶中)總共分爲3種存儲情況:空,鏈表頭,TreeBin頭
①遍歷原來的數組(原table),如果數組中某個值爲空,則直接放置一個forwordingNode(上篇博文介紹過)。
②如果數組中某個值不爲空,而是一個鏈表頭結點,那麼就對這個鏈表進行拆分爲兩個鏈表,存儲到nextTable對應的兩個位置。
③如果數組中某個值不爲空,而是一個TreeBin頭結點,那麼這個地方就存儲的是紅黑樹的結構,這樣一來,處理就會變得相對比較複雜,就需要先判斷需不需要把樹轉換爲鏈表,做完一系列的處理,然後把對應的結果存儲在nextTable的對應兩個位置。
在上一篇博文中介紹過,多個線程進行擴容操作的時候,會判斷原table的值,如果這個值是forwordingNode就表示這個節點被處理過了,就直接繼續往下找。接下來,我們針對源碼逐字逐句介紹:
/**
* 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; //stride 主要和CPU相關
//主要是判斷CPU處理的量,如果小於16則直接賦值16
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
if (nextTab == null) { // initiating只能有一個線程進行構造nextTable,如果別的線程進入發現不爲空就不用構造nextTable了
try {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]; //把新的數組變爲原來的兩倍,這裏的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;
//構造一個ForwardingNode用於多線程之間的共同擴容情況
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true; //遍歷的確認標誌
boolean finishing = false; // to ensure sweep before committing nextTab
//遍歷每個節點
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh; //定義一個節點和一個節點狀態判斷標誌fh
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing)
advance = false;
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
//下面就是一個CAS計算
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
//如果原table已經複製結束
if (finishing) {
nextTable = null; //可以看出在擴容的時候nextTable只是類似於一個temp用完會丟掉
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1); //修改擴容後的閥值,應該是現在容量的0.75倍
return;//結束循環
}
//採用CAS算法更新SizeCtl。
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算法獲取某一個數組的節點,爲空就設爲forwordingNode
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
//如果這個節點的hash值是MOVED,就表示這個節點是forwordingNode節點,就表示這個節點已經被處理過了,直接跳過
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
//對頭節點進行加鎖,禁止別的線程進入
synchronized (f) {
//CAS校驗這個節點是否在table對應的i處
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
//如果這個節點的確是鏈表節點
//把鏈表拆分成兩個小列表並存儲到nextTable對應的兩個位置
if (fh >= 0) {
int runBit = fh & n;
Node<K,V> lastRun = f;
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
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);
}
//CAS存儲在nextTable的i位置上
setTabAt(nextTab, i, ln);
//CAS存儲在nextTable的i+n位置上
setTabAt(nextTab, i + n, hn);
//CAS在原table的i處設置forwordingNode節點,表示這個這個節點已經處理完畢
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;
}
}
//如果拆分後的樹的節點數量已經少於6個就需要重新轉化爲鏈表
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;
//CAS存儲在nextTable的i位置上
setTabAt(nextTab, i, ln);
//CAS存儲在nextTable的i+n位置上
setTabAt(nextTab, i + n, hn);
//CAS在原table的i處設置forwordingNode節點,表示這個這個節點已經處理完畢
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}
PUT方法
再這之前,先簡單說一下PUT的具體操作:
①先傳入一個k和v的鍵值對,不可爲空(HashMap是可以爲空的),如果爲空就直接報錯。
②接着去判斷table是否爲空,如果爲空就進入初始化階段。
③如果判斷數組中某個指定的桶是空的,那就直接把鍵值對插入到這個桶中作爲頭節點,而且這個操作不用加鎖。
④如果這個要插入的桶中的hash值爲-1,也就是MOVED狀態(也就是這個節點是forwordingNode),那就是說明有線程正在進行擴容操作,那麼當前線程就進入協助擴容階段。
⑤需要把數據插入到鏈表或者樹中,如果這個節點是一個鏈表節點,那麼就遍歷這個鏈表,如果發現有相同的key值就更新value值,如果遍歷完了都沒有發現相同的key值,就需要在鏈表的尾部插入該數據。插入結束之後判斷該鏈表節點個數是否大於8,如果大於就需要把鏈表轉化爲紅黑樹存儲。
⑥如果這個節點是一個紅黑樹節點,那就需要按照樹的插入規則進行插入。
⑦put結束之後,需要給map已存儲的數量+1,在addCount方法中判斷是否需要擴容
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
//key和value都不可爲空,爲空直接拋出錯誤
if (key == null || value == null) throw new NullPointerException();
//計算Hash值,確定數組下標,這個和HashMap是一樣的,我再HashMap的第一篇有介紹過
int hash = spread(key.hashCode());
int binCount = 0;
//進入無線循環,直到插入爲止
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//如果table爲空或者容量爲0就表示沒有初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();//初始化數組
//CAS如果查詢數組的某個桶是空的,就直接插入該桶中
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這句話的意思是這個時候插入不用加鎖
}
//如果在插入的時候,節點是一個forwordingNode狀態,表示正在擴容,那麼當前線程進行幫助擴容
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;
//如果遍歷到一個值,這個值和當前的key是相同的,那就更改value值
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;
//如果遍歷到結束都沒有遇到相同的key,且後面沒有節點了,那就直接在尾部插入一個
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) {
//判斷節點數量是否大於8,如果大於就需要把鏈表轉化成紅黑樹
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//map已存儲的數量+1
addCount(1L, binCount);
return null;
}
其實,相對於transfer來說,PUT理解起來是不是簡單很多?說到transfer,咋在PUT方法中都沒出現過,只有一個helpTransfer(協助擴容)方法呢?其實,transfer方法放在了addCount方法中,下面是addCount方法的源碼:
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);
//如果小於0就說明已經再擴容或者已經在初始化
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();
}
}
}
GET方法
Get方法不論是在HashMap和ConcurrentHashMap都是最容易理解的,它的主要步驟是:
①先判斷數組的桶中的第一個節點是否尋找的對象是爲鏈表還是紅黑樹,
②如果是紅黑樹另外做處理
③如果是鏈表就先判斷頭節點是否爲要查找的節點,如果不是那麼就遍歷這個鏈表查詢
④如果都不是,那就返回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());
//數組已被初始化且指定桶中不爲空
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
//先判斷頭節點,如果頭節點的hash值與入參key的hash值相同
if ((eh = e.hash) == h) {
//頭節點的key就是傳入的key
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;//返回頭節點的value值
}
//eh<0表示這個節點是紅黑樹
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;//直接從樹上進行查找返回結果,不存在就返回null
//如果首節點不是查找對象且不是紅黑樹結構,那邊就遍歷這個列表
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
//都沒有找到就直接返回null值
return null;
}
好啦,ConcurrentHashMap我已經把自己學習到的都寫出來了,其實還有一些東西我沒能來得及寫出來,可能寫的時候,腦子完全被一個方法或者一個操作牽引住了,思維擴散不開。再者,博主也有很多沒有理解的地方,比如說在擴容的過程中,把一個鏈表拆分爲兩個鏈表到底是一個怎麼樣的過程,在HashMap的推理了一遍沒有理解,在ConcurrentHashmap也推理了一遍,還是沒有理解…各位無意間瀏覽到的大神,可以幫我指點一二嘛?