集合源碼之ConcurrentHashMap

引子

上篇解析過的hashmap不是線程安全的,因此并發大師Doug Lea在jdk1.5增加了concurrenthashmap類以滿足開發者在多線程環境安全使用map的需要。concurrenthashmap作為多線程環境常使用的類,它是如何實現線程安全的?但是又為什么說它不能完全替代hashtable?弱一致性又是怎樣一回事?讀完這篇解析concurrenthashmap的文章,我相信你會對concurrenthashmap的方方面面有一定的了解。
涉及到源碼的分析,一般我們都是先分析數據結構,然后在分析算法,數據結結構和算法都理解了,那么你就了解了大部分內容。類似上篇hashmap,對concurrenthashmap的分析也分為三個部分:

  • 第一部分:分析探究concurrenthashmap的數據結構。
  • 第二部分:分析concurrenthashmap的重要算法實現。
  • 第三部分:探究concurrenthashmap值得探討的問題。

為了簡單,跟hashmap類似的部分本文將一筆帶過,如果沒看過hashmap源碼可以看上篇文章集合源碼之hashmap,hashmap是concurrenthashmap的基礎,不建議沒讀過hashmap直接看本篇文章。本文分析的源碼采用的是concurrenthashmap較為簡單的jdk1.6版本。

concurrentHashMap的數據結構

首先我們來看concurrenthashmap數據結構的圖示,有個整體的認識。



圖里的是主要組成表示,我們更加具體的去分析concurrenthashmap的數據結構要分為concurrenthashmap的數據結構、segment的數據結構以及hashEntry的數據結構。重要的數據結構我們會解析用途以便大家更好的理解算法實現部分。

常量

  • DEFAULT_INITIAL_CAPACITY:默認初始容量16,與hashmap相同。
  • DEFAULT_LOAD_FACTOR:默認裝填因子0.75
  • DEFAULT_CONCURRENCY_LEVEL:默認并發級別16。其實就是segment個數,那為什么叫并發級別而不是segment數量呢?這里有必要細講一下,我們知道concurrenthashmap是通過分段鎖來實現高效并發的,這里的鎖就是指segment,每個segment持有一把鎖管理自身的數據。所以有幾個segment就有幾把鎖,也就是幾的并發級別。另外,segment初始化之后數量不會再改變。
  • MAXIMUM_CAPACITY:最大容量。
  • MAX_SEGMENTS:最大并發級別。
  • RETRIES_BEFORE_LOCK:查詢操作無鎖獲取嘗試次數。即某些查詢操作先不加鎖獲取,如果獲取的結果不對(獲取期間concurrenthashmap值變化)重試,如果超過嘗試次數,再進行加鎖獲取操作。

實例變量

  • segmentMask/segmentShift:計算對應的segment索引使用。
  • segments:對應的segment數組。
  • keySet/entrySet/values:對應的key、entry set集合。值的values集合。

segment

我們有必要對segment單獨的進行數據結構分析,因為主要的算法實現都是在segment中,segment是concurrenthashmap的靈魂??梢园裺egment看做是線程安全的hashmap,因為數據結構與hashmap非常相似。

  • 繼承 ReentrantLock:因此segment具有鎖的特性,可以對自身進行加鎖解鎖等操作。
  • count/modCount/threshold/loadfactor:數量、修改次數、擴容臨界值、裝填因子。
  • hashEntry[]:真正存儲數據的節點類數組。數據結構如開篇圖示。

concurrenthashmap重要算法實現

正如上文提到的,concurrenthashmap的靈魂是segment,segment的算法實現是整個concurrenthashmap的基礎,我們會著重分析segment的實現。

構造方法

多個構造方法,只討論最核心的構造方法。主要分為下面幾個步驟:

  • 校驗參數:校驗入參初始容量、裝填因子、并發級別合法性。
  • 根據并發級別確定segment數組大小:segment也得是2的指數大小。segmentmask為segment的大小-1,segmentshift為32減去調整并發級別次數。
  • 初始化segment:這里有段代碼值得討論。
        int c = initialCapacity / ssize;
        if (c * ssize < initialCapacity)
            ++c;
        int cap = 1;
        while (cap < c)
            cap <<= 1;

        for (int i = 0; i < this.segments.length; ++i)
            this.segments[i] = new Segment<K,V>(cap, loadFactor);

這段代碼計算cap的最終目的是為了確認初始化segment大小,讓cap與segment總數的乘積要大于等于初始容量的大小。

重要算法

需要說明的是除了全局的算法,大部分算法都是確定是哪個segment,然后調用segment方法。分為四類探討增刪改查,另外不接受null的value。

  • isEmpty():需要校驗所有的segment,所有算法實現要考慮的比hashmap要多。主要分為兩步:初次校驗、再次校驗。這兩步很多方法用到,需要著重理解。
    • 初次校驗: 遍歷所有segment,如果count不為0返回false,并記錄每個segment的modcount以及所有的modcount以便再次校驗。
    • 再次校驗:總modcount為0說明為空。不為0說明有值,那么就可能存在初次校驗時發生值修改的情況,需要再次校驗。遍歷segment,如果count不為0以及每個modcount發生修改就返回false。
  • size():即所有segment的count和。先不加鎖獲取,失敗加鎖獲取。
    • 不加鎖獲取:循環次數由RETRIES_BEFORE_LOCK決定。先遍歷segment記錄和、每個modcount以及總modcount。如果modcount不為0需要再次校驗,校驗時modcount不符則強制加鎖讀取,否則獲取第二次校驗和。兩次和相同即為size,不同再次循環。
    • 加鎖獲取:兩次的和不同的前提。遍歷segment加鎖、遍歷取和、遍歷解鎖。返回和值。
  • get(key):根據hash值找到對應的segment,然后調用segment的get方法。
    • segment---get(key,hash):如果count為0直接返回null。根據hash值得到對應entry的首節點,遍歷節點,沒找到返回null,找到了返回對應的value。但是這里需要注意的是get是不加鎖操作,可能get時有線程修改value導致對應key的value為null,這時候需要加鎖讀取了(readValueUnderLock(e))
  • containskey(key):根據hash值找到對應segment,調用contain方法
    • segment---containsKey(key,hash):如果count為0直接返回false。根據hash值得到對應entry數組索引的首節點。遍歷節點,存在返回true,否則返回false。
  • containsValue(value):因為value存在的segment不確定,需要遍歷所有的segment。與size的操作相同,先不加鎖執行,然后如果需要在加鎖執行。主要分為三步:
    • 不加鎖讀取:遍歷segment,調用segment的containsvalue方法,包含返回true,否則執行不加鎖校驗方法。
    • 不加鎖校驗:總modcount不為0,校驗每個segment的modcount,如果有不相同說明值發生改變需要加鎖讀取,如果modcount沒變說明不包含指定value,返回false。
    • 加鎖讀取:遍歷segment加鎖。遍歷segment調用containsvalue,存在返回true,否則返回false,遍歷解鎖。
增(put(key,value)/putIfAbsent(key,value))

增加方法也是調用segment的put(key,hash,value,boolean onlyIfAbsent)。onlyifabsent如果為true,則如果segment存在key不替換value。put默認替換value。put方法先lock,然后進行put操作。put方法跟hashmap類似,此處不贅述。

刪(remove(key)/remove(key,value))

刪除方法也是調用segment的remove(key,hash,value)方法。先找到要刪除的節點,找到后刪除節點,但是這里的刪除跟hashmap中的有所不同,因為hashentry的next是final的不可改變,所以只能新建節點,next指向刪除節點的next,然后克隆刪除節點前繼節點,這樣就完成了刪除操作。
另外clear方法是遍歷segment,調用segment的clear方法。clear方法加鎖,遍歷hashentry數組置null。

修改方法有兩個,一個是replace(key,value)調用segment的replace(key,hash,value),另一個是replace(key,oldvalue,newvalue)調用segment的replace(key,hash,oldvalue,newvalue)。
- segment---replace(key, hash, value):先加鎖,然后遍歷節點找到對應的節點,替換value。如果不存在key對應的節點就返回null。
- segment---replace(key, hash,oldvalue,newvalue):跟上面的方法類似,不同的是如果節點里的value跟傳入的oldvalue不同也不進行替換。

深入探討

concurrenthashmap的弱一致性

什么叫弱一致性?理想的強一致性就是放入一個元素后,立即獲取元素就是剛放入的元素,但是concurrenthashmap可能某時間段看不到這個元素。如下圖兩個線程執行順序:

則剛放入的元素可能會取不到。詳細的可以參考文章為什么ConcurrentHashMap是弱一致的

與hashtable、collections封裝后的SynchronizedMap比較

其實還可以加入concurrenthashmap的segment。

  • hashtable:使用synchronized關鍵字對方法進行修飾。
  • SynchronizedMap:使用一個mutex對象作為鎖,每次訪問方法必須要獲取mutex對象的鎖。
  • segment:使用ReentrantLock作為鎖,訪問方法時lock進行加鎖
  • concurrenthashmap:多個segment。
    一對比就可以看出concurrenthashmap引入并發級別的策略,可以讓concurrenthashmap在多線程環境的性能更加突出,速度也更快。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 前言 JDK中的Hashtable是一個線程安全的K-V形式的容器,它實現線程安全的原理十分簡單,就是在所有涉及對...
    Justlearn閱讀 2,338評論 0 11
  • java筆記第一天 == 和 equals ==比較的比較的是兩個變量的值是否相等,對于引用型變量表示的是兩個變量...
    jmychou閱讀 1,518評論 0 3
  • ConcurrencyMap 從這一節開始正式進入并發容器的部分,來看看JDK 6帶來了哪些并發容器。 在JDK ...
    raincoffee閱讀 565評論 0 0
  • 引言 ConcurrentHashMap是線程安全并且高效的HashMap,在并發編程中經??梢娝氖褂?,在開始分...
    miaoLoveCode閱讀 15,912評論 14 40
  • 水平不高,但是喜歡寫字,比如簽名、短句…… 如果喜歡的話,評論名字幫寫,歡迎打擾!新人求關注啦~~
    社會小學生閱讀 749評論 12 4