原創: 許光明 杏仁技術站 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 更適合做爲線程安全的 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 類型限制最大值。
全文完
如下文章您可能也會感興趣: