簡書 占小狼
轉載請注明原創出處,謝謝!
知止而后有定,定而后能靜,靜而后能安,安而后能慮,慮而后能得。
ConcurrentHashMap
在多線程環境下,使用HashMap
進行put
操作時存在丟失數據的情況,為了避免這種bug的隱患,強烈建議使用ConcurrentHashMap
代替HashMap
,為了對更深入的了解,本文將對JDK1.7和1.8的不同實現進行分析。
JDK1.7
數據結構
jdk1.7中采用Segment
+ HashEntry
的方式進行實現,結構如下:
ConcurrentHashMap
初始化時,計算出Segment
數組的大小ssize
和每個Segment
中HashEntry
數組的大小cap
,并初始化Segment
數組的第一個元素;其中ssize
大小為2的冪次方,默認為16,cap
大小也是2的冪次方,最小值為2,最終結果根據根據初始化容量initialCapacity
進行計算,計算過程如下:
if (c * ssize < initialCapacity)
++c;
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
cap <<= 1;
其中Segment
在實現上繼承了ReentrantLock
,這樣就自帶了鎖的功能。
put實現
當執行put
方法插入數據時,根據key的hash值,在Segment
數組中找到相應的位置,如果相應位置的Segment
還未初始化,則通過CAS進行賦值,接著執行Segment
對象的put
方法通過加鎖機制插入數據,實現如下:
場景:線程A和線程B同時執行相同Segment
對象的put
方法
1、線程A執行tryLock()
方法成功獲取鎖,則把HashEntry
對象插入到相應的位置;
2、線程B獲取鎖失敗,則執行scanAndLockForPut()
方法,在scanAndLockForPut
方法中,會通過重復執行tryLock()
方法嘗試獲取鎖,在多處理器環境下,重復次數為64,單處理器重復次數為1,當執行tryLock()
方法的次數超過上限時,則執行lock()
方法掛起線程B;
3、當線程A執行完插入操作時,會通過unlock()
方法釋放鎖,接著喚醒線程B繼續執行;
size實現
因為ConcurrentHashMap
是可以并發插入數據的,所以在準確計算元素時存在一定的難度,一般的思路是統計每個Segment
對象中的元素個數,然后進行累加,但是這種方式計算出來的結果并不一樣的準確的,因為在計算后面幾個Segment
的元素個數時,已經計算過的Segment
同時可能有數據的插入或則刪除,在1.7的實現中,采用了如下方式:
try {
for (;;) {
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;
}
}
if (sum == last)
break;
last = sum;
}
} finally {
if (retries > RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
segmentAt(segments, j).unlock();
}
}
先采用不加鎖的方式,連續計算元素的個數,最多計算3次:
1、如果前后兩次計算結果相同,則說明計算出來的元素個數是準確的;
2、如果前后兩次計算結果都不同,則給每個Segment
進行加鎖,再計算一次元素的個數;
JDK1.8
數據結構
1.8中放棄了Segment
臃腫的設計,取而代之的是采用Node
+ CAS
+ Synchronized
來保證并發安全進行實現,結構如下:
只有在執行第一次put
方法時才會調用initTable()
初始化Node
數組,實現如下:
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;
}
put實現
當執行put
方法插入數據時,根據key的hash值,在Node
數組中找到相應的位置,實現如下:
1、如果相應位置的Node
還未初始化,則通過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
}
2、如果相應位置的Node
不為空,且當前該節點不處于移動狀態,則對該節點加synchronized
鎖,如果該節點的hash
不小于0,則遍歷鏈表更新節點或插入新節點;
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
;
size實現
1.8中使用一個volatile
類型的變量baseCount
記錄元素的個數,當插入新數據或則刪除數據時,會通過addCount()
方法更新baseCount
,實現如下:
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();
}
1、初始化時counterCells
為空,在并發量很高時,如果存在兩個線程同時執行CAS
修改baseCount
值,則失敗的線程會繼續執行方法體中的邏輯,使用CounterCell
記錄元素個數的變化;
2、如果CounterCell
數組counterCells
為空,調用fullAddCount()
方法進行初始化,并插入對應的記錄數,通過CAS
設置cellsBusy字段,只有設置成功的線程才能初始化CounterCell
數組,實現如下:
else if (cellsBusy == 0 && counterCells == as &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean init = false;
try { // Initialize table
if (counterCells == as) {
CounterCell[] rs = new CounterCell[2];
rs[h & 1] = new CounterCell(x);
counterCells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
3、如果通過CAS
設置cellsBusy字段失敗的話,則繼續嘗試通過CAS
修改baseCount
字段,如果修改baseCount
字段成功的話,就退出循環,否則繼續循環插入CounterCell
對象;
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
break;
所以在1.8中的size
實現比1.7簡單多,因為元素個數保存baseCount
中,部分元素的變化個數保存在CounterCell
數組中,實現如下:
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;
}
通過累加baseCount
和CounterCell
數組中的數量,即可得到元素的總個數;
我是占小狼,如果讀完覺得有收獲的話,歡迎點贊加關注