前言
HashMap.size()
的代碼非常簡單,直接返回成員變量size即可。可是在ConcurrentHashMap
里面,是否也是這樣呢?答案是否定的,因為我們需要考慮并發的情況。若我們繼續沿用HashMap.size()
的思想,并發的效率可以想象得到,將變得非常得低效。
那么ConcurrentHashMap.size()
又是怎么實現的呢?我們這一小節將進行分析~
size()
這個方法內部調用sumCount()
方法,但是sumCount()
返回的是long類型的,而size()
返回的是int類型的,需要進行一定的適配。
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
sumCount()
這個方法用于返回節點的個數,有用到兩個關鍵的成員變量
-
baseCount
-- 基本的計數器,當沒有競爭的時候,使用這個值 -
counterCells
-- 數組結構,當有競爭的時候使用它來進行計算
sumCount()
實現的邏輯如下:
- 首先判斷
counterCells
是否為null,為null說明沒有競爭,直接返回baseCount
。 - 如果
counterCells
不為null,說明有競爭,通過遍歷counterCells
數組,將所有元素的value
進行求和。
/**
* Base counter value, used mainly when there is no contention,
* but also as a fallback during table initialization
* races. Updated via CAS.
* 基本的計數器,當沒有競爭的時候,使用這個值
*/
private transient volatile long baseCount;
/**
* Table of counter cells. When non-null, size is a power of 2.
*/
private transient volatile CounterCell[] counterCells;
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;
}
既然競爭的時候需要用到counterCells
,那么它又是什么時候生成的呢?在前面的分析我們看到了counterCells
的影子,那就是在addCount()
方法中,那么接下來我們就再來分析一下關于counterCells
的代碼。
addCount()方法再分析
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
// 1. 初始的時候,counterCells一定為null,第一個條件此時將不會被滿足
// 2. 在第一個條件不滿足的時候,需要判斷第二個條件。
// 3. 第二個條件在存在競爭的情況下將返回false,也就是需要進入到if里面
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
// 這兒有三個或條件,只需要滿足其一,就可以進入到if
// 1. counterCells為null
// 2. counterCells為空數組
// 3. counterCells指定位置元素為null
// 4. CAS設置counterCell.value失敗的時候
// 這幾種情況將進入到`fullAddCount()`進行自旋操作
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();
}
// 省略掉擴容的代碼...
}
fullAddCount()方法
這個方法非常得復雜,代碼也很長,如果深入進去將陷入到細節中去。在此我們只需要知道這個方法的用途,那就是通過自旋的方式新增或更新counterCells
,使用counterCells
來統計節點個數將不會得到準確的結果。
CounterCell類
既然競爭的情況下是圍繞counterCells
來展開的,那么我們需要看看這個成員變量所屬的類型,CounterCell
。
/**
* A padded cell for distributing counts. Adapted from LongAdder
* and Striped64. See their internal docs for explanation.
* 改編自LongAdder和Striped64
*/
@sun.misc.Contended static final class CounterCell {
volatile long value;
CounterCell(long x) { value = x; }
}
這個類有幾個需要注意的地方
-
value
使用volatile
修飾,可保證其可見性 -
CounterCell
使用@sun.misc.Contended
修飾
@sun.misc.Contended
這個注釋需要明確說明一下。如果類上面使用該注釋,標識需要防止偽共享
。
那么什么又是偽共享
呢?借鑒一下網上大神的闡述。
【說明】:偽共享(false sharing)。先引用對偽共享的解釋
緩存系統中是以緩存行(cache line)為單位存儲的。緩存行是2的整數冪個連續字節,
一般為32-256個字節。最常見的緩存行大小是64個字節。當多線程修改互相獨立的變量時,
如果這些變量共享同一個緩存行,就會無意中影響彼此的性能,這就是偽共享。
JDK 8版本之前沒有這個注解,Doug Lea使用拼接來解決這個問題,把緩存行加滿,讓緩存之間的修改互不影響。
mappingCount()
我們額外分析一下這個方法,這是jdk8新加的一個方法,內部同樣調用了sumCount()
,和size()
有同樣的效果,但是它的返回值卻是long,可以避免精度的損失。同時這個方法也是被推薦用來替代size()
。
/**
* Returns the number of mappings. This method should be used
* instead of {@link #size} because a ConcurrentHashMap may
* contain more mappings than can be represented as an int. The
* value returned is an estimate; the actual count may differ if
* there are concurrent insertions or removals.
*
* @return the number of mappings
* @since 1.8
*/
public long mappingCount() {
long n = sumCount();
return (n < 0L) ? 0L : n; // ignore transient negative values
}
總結
期初,我們還以為size()
會很簡單,但是一通分析下來卻發現它并不是想象得那么簡單,尤其是addCount()
內部調用的fullAddCount()
方法,其復雜度不下于擴容方法。
為了省事,本節并沒有對這個方法進行分析,后續有時間再來分析。