ConcurrentHashMap中put()這個方法很容易引起并發操作的問題,現在來研究下put()方法的實現
put方法
public V put(K key, V value) {
return putVal(key, value, false);
}
//onlyIfAbsent跟HashMap一樣,就是判斷是否要覆蓋,默認為false,覆蓋
final V putVal(K key, V value, boolean onlyIfAbsent) {
//這句話可以看出,ConcurrentHashMap中不允許存在空值,這個是跟HashMap的區別之一
//通過這個機制,我們可以通過get方法獲取一個key,如果拋出異常,說明這個key不存在
if (key == null || value == null) throw new NullPointerException();
//ConcurrentHashMap中的hash值計算方法,跟HashMap中的差不多,不過最后的結果要
//與0x7fffffff進行與操作,這個地方我還不明白為什么有這個與操作
int hash = spread(key.hashCode());
//binCount=0說明首節點插入,未進行鏈表或紅黑樹操作,因為后面會對這個值進行更改
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//如果數組為空或者長度為0,進行初始化工作
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
}
//如果hash值等于MOVEN(默認-1),說明是協助擴容,這個在后邊講解ForwardingNode類
//進行進一步解析
else if ((fh = f.hash) == MOVED)
//協助擴容
tab = helpTransfer(tab, f);
else {
V oldVal = null;
//對桶的首節點進行加鎖
synchronized (f) {
//雙重判定,為了防止在當前線程進來之前,i地址所對應對象已經更改
if (tabAt(tab, i) == f) {
//為什么fh一定要大于等于0,這個原因在于TreeBin的hash值的設定,TreeBin類型
//的hash值默認設置為了-2
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;
//當到桶的結尾還沒找到,則新增一個Node
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
//如果hash小于0,判斷是否是TreeBin的實例
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;
}
}
}
}
//如果binCount值不等于0,說明進行了鏈表或者紅黑樹操作
if (binCount != 0) {
//如果binCount大于8則進行樹化,但真正的轉換成紅黑樹不是8的長度
//當長度超過64才會真正的樹化,處于8-64之間的還只是數組擴容
if (binCount >= TREEIFY_THRESHOLD)
//這個方法我在紅黑樹一節詳細講解了,不清楚的可以看紅黑樹那節
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//計數方法
addCount(1L, binCount);
return null;
}
我對這個方法進行了注釋,可以直觀的看代碼進行了解,主要分析一下三個方法
1、數組初始化方法initTable()
2、線程協助擴容方法helpTransfer()
3、計數方法addCount()
數組初始化initTable()
我們來看下這個方法的代碼:
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
//判斷是否數組為空或者數組長度為0(未初始化)
while ((tab = table) == null || tab.length == 0) {
//如果sizeCtl值小于0,則每個進入到這里的線程要作線程讓步操作
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
//SIZECTL是地址偏移量,和前邊我們講到的tabAt()那三個方法的地址偏移量一樣
//如果SIZECTL對應地址的值與sc相等,說明當前的線程是第一個到達這條語句的
//線程,那么就會將SIZECTL地址所對應的值替換成-1,而SIZECTL地址偏移量對應的
// 對象就是sizeCtl。
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
//再次進行判斷,防止在進行U.compareAndSwapInt(this, SIZECTL, sc, -1)的時候
//有其他線程并發進入方法,導致出錯,使用雙重鎖
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設置成為當前容量類似hashmap中閾值threshold
sizeCtl = sc;
}
break;
}
}
return tab;
}
這個地方有點疑問,sizeCtl到底指代什么,是閾值還是容量,在傳入容量的構造方法中,是直接將cap賦值給sizeCtl,也就是說在這里的sizeCtl是容量大小,而數組初始化的時候,又將容量的0.75倍賦值給了sizeCtl,找了下網上的資料
引用這個鏈接(http://www.lxweimin.com/p/c0642afe03e0)的說法:
sizeCtl :默認為0,用來控制table的初始化和擴容操作,具體應用在后續會體現出來。
**-1 **代表table正在初始化
**-N **表示有N-1個線程正在進行擴容操作
其余情況:
1、如果table未初始化,表示table需要初始化的大小。
2、如果table初始化完成,表示table的容量,默認是table大小的0.75倍,居然用這個公式算0.75(n - (n >>> 2))。
也就是說,sizeCtl的值有這幾種情況,跟hashmap中單一指代閾值和容量的不同
線程協助擴容方法helpTransfer()
講這個方法之前,我們需要先了解一些其他的類與方法,首先是ForwardingNode,一個特殊的Node節點,hash值為-1,其中存儲nextTable的引用。
static final class ForwardingNode<K,V> extends Node<K,V> {
final Node<K,V>[] nextTable;
ForwardingNode(Node<K,V>[] tab) {
//MOVEN默認值-1
super(MOVED, null, null, null);
this.nextTable = tab;
}
Node<K,V> find(int h, Object k){...}
}
然后是方法resizeStamp(),這個方法是計算校驗碼的
static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
好,現在我們來看下helpTransfer()方法的使用:
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;
}
首先判斷是否能夠協助擴容的條件
1、數組不為空
2、傳入的首節點是ForwardingNode的實例
3、該實例的nextTable類型不為空
在這三個條件的前提下,進行下一步,通過resizeStamp()方法拿到一個校驗碼,然后再判斷sizeCtl的值是否小于0,因為sizeCtl負數表示正在擴容。
停止擴容的四個條件(這部分我還沒理解,等后面理解了回來補充):
1、(sc >>> RESIZE_STAMP_SHIFT) != rs
2、sc == rs + 1
3、 sc == rs + MAX_RESIZERS
4、transferIndex <= 0
如果符合擴容條件,就使用U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)在sc上加一,然后調用transfer()方法進行數據遷移,這個方法比較復雜,到時候拿出來單獨講.
參考鏈接:
https://blog.csdn.net/u011392897/article/details/60479937
計數方法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);
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();
}
}
}
這個方法分為兩部分,第一部分是計數,第二部分是擴容
第一部分計數,
當counterCells為空并且U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)返回true,則直接到下一部分是否擴容的判斷,否則 CAS 失敗后轉入更新 counterCells ,防止CAS 自旋,使用的方法是fullAndCount(),這個方法取自striped64的LongerAdd的方法。
關于striped64的解析詳情可以看以下鏈接:
http://brokendreams.iteye.com/blog/2259857
說到這個,我就不得不提一下這個類CounterCell,這個類代碼如下:
@sun.misc.Contended static final class CounterCell {
volatile long value;
CounterCell(long x) { value = x; }
}
我們看到這個類用@sun.misc.Contended進行了注釋,這個注釋是自動填充緩存行用的,為什么要自動填充緩存行?
因為這樣可以防止偽共享的情況,那么什么是偽共享呢?
我解釋一下,就是當多線程修改互相獨立的變量時,如果這些變量共享同一個緩存行,就會無意中影響彼此的性能,這就是偽共享,也就是避免頭結點和為節點加載到同一個緩存行,使頭尾節點在修改時不會互相鎖定,基于此,我們需要自動填充緩存行。
這個類中我們可以看到只有一個volatile修飾符的變量value,也就是單個counterCell對象累加值
第二部分擴容,
滿足當前容量大于sizeCtl值并且數組不為空,數組長度小于最大容量值的時候,就開始擴容,跟helpTransfer()方法中一樣,這里也進行了同樣的判定,當線程剛進來的時候,sc是正的,所以執行else if的語句,U.compareAndSwapInt(this, SIZECTL, sc,(rs << RESIZE_STAMP_SHIFT) + 2)
這條語句將sizeCtl直接賦值成了一個負數,如果賦值成功,則調用數據遷移方法transfer().
在代碼的最后有個s = sumCount()的語句,這個是ConcurrentHashmap內部計數完反饋給我們的值,在size()中也有相關調用:
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
我們來看下sumCount()方法的內部實現:
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;
}
可以看出這個計算是分為兩部分,基礎部分是baseCount部分,然后還要加上countCells數組的所有值,通過之前的分析我們知道,baseCount是CAS成功后會自動累加的值,而countCells數組是在CAS失敗,也就是出現并發的情況下,進行累加的數組,這個數組類似segmenet一樣,采用分段鎖,最后將二者的值相加從而得到一個近似值。