ConcurrentHashMap 的 size 方法原理分析

ConcurrentHashMap 的 size 方法原理分析

原創: 許光明 杏仁技術站 1周前程序員

做者 | 許光明算法

杏仁後端工程師。少青年程序員,關注服務端技術和農藥。後端

前言

JAVA 語言提供了大量豐富的集合, 好比 List, Set, Map 等。其中 Map 是一個經常使用的一個數據結構,HashMap 是基於 Hash 算法實現 Map 接口而被普遍使用的集類。HashMap 裏面是一個數組,而後數組中每一個元素是一個單向鏈表。可是 HashMap 並非線程安全的, 在多線程場景下使用存在併發和死循環問題。HashMap 結構如圖所示:數組

線程安全的解決方案

線程安全的 Map 的實現有 HashTable 和 ConcurrentHashMap 等。HashTable 對集合讀寫操做經過 Synchronized 同步保障線程安全, 整個集合只有一把鎖, 對集合的操做只能串行執行,性能不高。ConcurrentHashMap 是另外一個線程安全的 Map, 一般來講他的性能優於 HashTable。 ConcurrentHashMap 的實如今 JDK1.7 和 JDK 1.8 有所不一樣。緩存

在 JDK1.7 版本中,ConcurrentHashMap 的數據結構是由一個 Segment 數組和多個 HashEntry 組成。簡單理解就是ConcurrentHashMap 是一個 Segment 數組,Segment 經過繼承 ReentrantLock 來進行加鎖,因此每次須要加鎖的操做鎖住的是一個 Segment,這樣只要保證每一個 Segment 是線程安全的,也就實現了全局的線程安全。安全

JDK1.8 的實現已經摒棄了 Segment 的概念,而是直接用 Node 數組 + 鏈表 + 紅黑樹的數據結構來實現,併發控制使用 Synchronized 和 CAS 來操做,整個看起來就像是優化過且線程安全的 HashMap,雖然在 JDK1.8 中還能看到 Segment 的數據結構,可是已經簡化了屬性,只是爲了兼容舊版本。 經過 HashMap 查找的時候,根據 hash 值可以快速定位到數組的具體下標,若是發生 Hash 碰撞,須要順着鏈表一個個比較下去才能找到咱們須要的,時間複雜度取決於鏈表的長度,爲 O(n)。爲了下降這部分的開銷,在 Java8 中,當鏈表中的元素超過了 8 個之後,會將鏈表轉換爲紅黑樹,在這些位置進行查找的時候能夠下降時間複雜度爲 O(logN)。性能優化

如何計算 ConcurrentHashMap Size

由上面分析可知,ConcurrentHashMap 更適合做爲線程安全的 Map。在實際的項目過程當中,咱們一般須要獲取集合類的長度, 那麼計算 ConcurrentHashMap 的元素大小就是一個有趣的問題,由於他是併發操做的,就是在你計算 size 的時候,它還在併發的插入數據,可能會致使你計算出來的 size 和你實際的 size 有差距。本文主要分析下 JDK1.8 的實現。 關於 JDK1.7 簡單提一下。數據結構

在 JDK1.7 中,第一種方案他會使用不加鎖的模式去嘗試屢次計算 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 實現相比 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 方法中,會繼續死循環操做,直到成功。

而後,CounterCell 這個類究竟是什麼?咱們會發現它使用了 @sun.misc.Contended 標記的類,內部包含一個 volatile 變量。@sun.misc.Contended 這個註解標識着這個類防止須要防止 "僞共享"。那麼,什麼又是僞共享呢?

緩存系統中是以緩存行(cache line)爲單位存儲的。緩存行是2的整數冪個連續字節,通常爲32-256個字節。最多見的緩存行大小是64個字節。當多線程修改互相獨立的變量時,若是這些變量共享同一個緩存行,就會無心中影響彼此的性能,這就是僞共享。

CounterCell 代碼以下:

@sun.misc.Contended static final class CounterCell {
    volatile long value;
    CounterCell(long x) { value = x; }
}    

總結

 

  • JDK1.7 和 JDK1.8 對 size 的計算是不同的。 1.7 中是先不加鎖計算三次,若是三次結果不同在加鎖。

  • JDK1.8 size 是經過對 baseCount 和 counterCell 進行 CAS 計算,最終經過 baseCount 和 遍歷 CounterCell 數組得出 size。

  • JDK 8 推薦使用mappingCount 方法,由於這個方法的返回值是 long 類型,不會由於 size 方法是 int 類型限制最大值。

 

全文完

 

如下文章您可能也會感興趣:

相關文章
相關標籤/搜索