1. 程式人生 > 實用技巧 >ConcurrentHashMap size 方法

ConcurrentHashMap size 方法

JDK1.7 版本

ConcurrentHashMap 的資料結構是由一個 Segment 陣列和多個 HashEntry 組成。簡單理解就是ConcurrentHashMap 是一個 Segment 陣列,Segment 通過繼承 ReentrantLock 來進行加鎖,所以每次需要加鎖的操作鎖住的是一個 Segment,這樣只要保證每個 Segment 是執行緒安全的,也就實現了全域性的執行緒安全。

如何計算 ConcurrentHashMap Size

由上面分析可知,ConcurrentHashMap 更適合作為執行緒安全的 Map。在實際的專案過程中,我們通常需要獲取集合類的長度, 那麼計算 ConcurrentHashMap 的元素大小就是一個有趣的問題,因為他是併發操作的,就是在你計算 size 的時候,它還在併發的插入資料,可能會導致你計算出來的 size 和你實際的 size 有差距

在 JDK1.7 中, 第一種方案: 他會使用不加鎖的模式去嘗試2次計算 ConcurrentHashMap 的 size,比較前後兩次計算的結果,結果一致就認為當前沒有元素加入,計算的結果是準確的。 第二種方案: 是如果第一種方案不符合,他就會給每個 Segment 加上鎖,然後計算 ConcurrentHashMap 的 size 返回。其原始碼實現:
public int size() {
  final Segment<K,V>[] segments = this.segments;
  int size;
  boolean overflow; // true if size overflows 32 bits
long sum; // sum of modCounts long last = 0L; // previous sum int retries = -1; // first iteration isn't retry 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(); } } return overflow ? Integer.MAX_VALUE : size; }

JDK1.8 實現已經摒棄了 Segment 的概念,而是直接用 Node 陣列 + 連結串列 + 紅黑樹的資料結構來實現,併發控制使用 Synchronized 和 CAS 來操作,整個看起來就像是優化過且執行緒安全的 HashMap,雖然在 JDK1.8 中還能看到 Segment 的資料結構,但是已經簡化了屬性,只是為了相容舊版本。 通過 HashMap 查詢的時候,根據 hash 值能夠快速定位到陣列的具體下標,如果發生 Hash 碰撞,需要順著連結串列一個個比較下去才能找到我們需要的,時間複雜度取決於連結串列的長度,為 O(n)。為了降低這部分的開銷,在 Java8 中,當連結串列中的元素超過了 8 個以後,會將連結串列轉換為紅黑樹,在這些位置進行查詢的時候可以降低時間複雜度為 O(logN)。

如何計算 ConcurrentHashMap Size

JDK1.8 實現相比 JDK 1.7 簡單很多,只有一種方案,我們直接看size()程式碼:

public int size() {
    long n = sumCount();
    return ((n < 0L) ? 0 :
           (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int)n);
}

最大值是 Integer 型別的最大值,但是 Map 的 size 可能超過 MAX_VALUE, 所以還有一個方法mappingCount(),JDK 的建議使用mappingCount()而不是size()mappingCount()的程式碼如下:

public long mappingCount() {
    long n = sumCount();
    return (n < 0L) ? 0L : n; // ignore transient negative values
}

以上可以看出,無論是size()還是mappingCount(), 計算大小的核心方法都是sumCount()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;
}

分析一下sumCount()程式碼。ConcurrentHashMap 提供了 baseCount、counterCells 兩個輔助變數和一個 CounterCell 輔助內部類。sumCount()就是迭代 counterCells 來統計 sum 的過程。

put 操作時,肯定會影響size(),在put()方法最後會呼叫addCount()方法。

addCount()程式碼如下:

如果 counterCells == null, 則對 baseCount 做 CAS 自增操作。

如果併發導致 baseCount CAS 失敗了使用 counterCells。

如果counterCells CAS 失敗了,在 fullAddCount 方法中,會繼續死迴圈操作,直到成功。

JDK1.8 size 是通過對 baseCount 和 counterCell(專門處理CAS失敗的操作,最終都是成功的因為迴圈CAS操作),最終通過 baseCount 和 遍歷 CounterCell 陣列得出 size。