1. 程式人生 > >ConcurrentHashMap 的 size 方法原理分析

ConcurrentHashMap 的 size 方法原理分析

作者 | 許光明

640?wx_fmt=jpeg

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

前言

JAVA 語言提供了大量豐富的集合, 比如 List, Set, Map 等。其中 Map 是一個常用的一個數據結構,HashMap 是基於 Hash 演算法實現 Map 介面而被廣泛使用的集類。HashMap 裡面是一個數組,然後陣列中每個元素是一個單向連結串列。但是 HashMap 並不是執行緒安全的, 在多執行緒場景下使用存在併發和死迴圈問題。HashMap 結構如圖所示:

640?wx_fmt=png

執行緒安全的解決方案

執行緒安全的 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 是執行緒安全的,也就實現了全域性的執行緒安全。

640?wx_fmt=png

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

640?wx_fmt=png

如何計算 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 自增操作。

640?wx_fmt=jpeg

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

640?wx_fmt=jpeg

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

640?wx_fmt=jpeg

然後,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 型別限制最大值。

全文完

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

我們正在招聘 Java 工程師,歡迎有興趣的同學投遞簡歷到 [email protected]

640?wx_fmt=png

杏仁技術站

長按左側二維碼關注我們,這裡有一群熱血青年期待著與您相會。

相關推薦

concurrenthashmapsize方法原理

同上,這也是同一個面試的時候別人問的,我只是記得看過,在concurrenthashmap中會統計多次,當時就說會統計兩次進行比較,人家接著問為啥。。。我傻了一下,這不是明擺著兩次統計的中間有新的變化了,會導致統計不準確嗎?當時也不知道說啥好,以為他有新的點,就

ConcurrentHashMap原理分析

技術HashTable是一個線程安全的類,它使用synchronized來鎖住整張Hash表來實現線程安全,即每次鎖住整張表讓線程獨占。ConcurrentHashMap允許多個修改操作並發進行,其關鍵在於使用了鎖分離技術。它使用了多個鎖來控制對hash表的不同部分進行的修改。ConcurrentHashMa

Android 65K問題之Multidex原理分析及NoClassDefFoundError的解決方法

bottom mini ati ... types auto weight right for Android 65K問題相信困惑了不少人,盡管AS的出來能夠通過分dex高速解決65K問題,可是同一時候也easy由於某些代碼沒有打包到MainDex裏

HashMap,ConcurrentHashMap 原理分析

帶環鏈表 原理 擴展 安全 nbsp adf java 線程 cit ----基於Java1.7的 HashMap原理 1.基於哈希原理,存儲key-value鍵值對(Entry)的集合。在JDK1.8以前數據結構是一個數組+鏈表,在JDK1.8以後是一個數組+鏈表+紅黑樹

HashMap底層原理分析(put、get方法

return sta rec oca ati 技術分享 AI TP load 1、HashMap底層原理分析(put、get方法) HashMap底層是通過數組加鏈表的結構來實現的。HashMap通過計算key的hashCode來計算hash值,只要hashCode一樣

多工學習概述論文:從定義和方法到應用和原理分析

多工學習是一個很有前景的機器學習領域,相關的理論和實驗研究成果以及應用也在不斷湧現。近日,香港科技大學電腦科學與工程系的楊強教授和張宇助理教授在《國家科學評論(National Science Review)》2018 年 1 月份釋出的「機器學習」專題期刊中發表了題為《An overview of

osgEarth的Rex引擎原理分析(二十)osgEarth::TerrainEngineNode中setMap方法作用

目標:(十二)中的問題12 不同於派生類RexTerrainEngineNode中setMap的內容(詳見(十二)),這裡主要完成以下工作: 1、設定地圖圖層_map 2、   建立地形瓦片模型工廠_tileModelFactory,用於建立覆蓋紋理、高程紋理、影像紋理

ConcurrentHashMap & HashMap最清晰的底層原理分析(基於JDK1.7跟1.8比較)

前言 Map 這樣的 Key  Value 在軟體開發中是非常經典的結構,常用於在記憶體中存放資料。 本篇主要想討論 ConcurrentHashMap 這樣一個併發容器,在正式開始之前我覺得有必要談談 HashMap,沒有它就不會有後面的 ConcurrentHashM

Java集合---ConcurrentHashMap原理分析

背景 執行緒不安全的HashMap 因為多執行緒環境下,使用Hashmap進行put操作會引起死迴圈,導致CPU利用率接近100%,所以在併發情況下不能使用HashMap。 效率低下的HashTable容器 HashTable容器使用synchronized來

關於ToolRunner.run()方法執行Hadoop程式原理分析

                        關於ToolRunner.run()方法執行Hadoop程式原理分析 文章開始把我喜歡的這句話送個大家:這個世界上還有

Android Handler 機制以及各方法所線上程原理分析

Handler 的定義及作用: 因為有的文章已經說得比較清楚了,就直接引用下。這裡借鑑http://mobile.51cto.com/aprogram-442833.htm 一、Handler的定義: 主要接受子執行緒傳送的資料, 並用此資料配合主執行緒更新UI。

關於未將物件引用設定到物件例項簡單原理分析,與解決方法

這個問題我相信困擾著許多的,剛剛入門面向物件思想的同學。 其實這個問題沒有想象中的那麼複雜,只是太多同學把寫程式碼想得太過於快餐。不知道現在還有多少同學在處錯誤的時候,先讀完錯誤提示再去看怎麼錯的呢? 下面我來給大家分析一下這個“未將物件引用設定到物件例項”這個問題。 在我

Java集合---ConcurrentHashMap原理分析(面試問題:ConcurrentHashMap實現原理是怎麼樣的)

集合是程式設計中最常用的資料結構。而談到併發,幾乎總是離不開集合這類高階資料結構的支援。比如兩個執行緒需要同時訪問一箇中間臨界區(Queue),比如常會用快取作為外部檔案的副本(HashMap)。這篇文章主要分析jdk1.5的3種併發集合型別(concurrent,cop

直接繼承View來自定義控制元件時,需要重寫onMeasure()方法並設定wrap_content時的大小 原理分析

        之前在校學習的時候,一直沒有在網上找到比較靠譜的解釋,現在畢業了,程式設計能力也比之前有了不小的提高,就讀了一些原始碼,加上一些書上的解釋,現在算是大體知道原因了吧!如果哪裡說的不對,歡迎批評指正。        在開始本篇的正文之前,請允許我先粗略的解釋一

hashmap衝突的解決方法以及原理分析

在Java程式語言中,最基本的結構就是兩種,一種是陣列,一種是模擬指標(引用),所有的資料結構都可以用這兩個基本結構構造,HashMap也一樣。當程式試圖將多個 key-value 放入 HashMap 中時,以如下程式碼片段為例: HashMap<String,

hashmap衝突的解決方法以及原理分析

在Java程式語言中,最基本的結構就是兩種,一種是陣列,一種是模擬指標(引用),所有的資料結構都可以用這兩個基本結構構造,HashMap也一樣。當程式試圖將多個 key-value 放入 HashMap 中時,以如下程式碼片段為例: HashMap<String,Object> m=new Ha

Java集合---ConcurrentHashMap原理分析(轉)

    轉載自: http://www.cnblogs.com/ITtangtang/p/3948786.html       感謝作者 集合是程式設計中最常用的資料結構。而談到併發,幾乎總是離不開集合這類高階資料結構的支援。比如兩個執行緒需要同時訪問一箇中間臨

【轉載】ConcurrentHashMap原理分析

  曾經在 [高併發Java 五]JDK併發包1 中提到過ConcurrentHashMap,只是簡單的提到了下ConcurrentHashMap的優點,以及大概的實現原理。   而本文則重點介紹ConcurrentHashMap實現的細節。   Ha

ConcurrentHashmap實現原理分析

ConcurrentHashmap 重寫equals方法需同時重寫hashCode方法 object物件中的 public boolean equals(Object obj),對於任何非空引用值 x 和 y,當且僅當 x 和 y 引用同一個物件