引子
上篇解析過的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可能某時間段看不到這個元素。如下圖兩個線程執行順序:
與hashtable、collections封裝后的SynchronizedMap比較
其實還可以加入concurrenthashmap的segment。
- hashtable:使用synchronized關鍵字對方法進行修飾。
- SynchronizedMap:使用一個mutex對象作為鎖,每次訪問方法必須要獲取mutex對象的鎖。
- segment:使用ReentrantLock作為鎖,訪問方法時lock進行加鎖
-
concurrenthashmap:多個segment。
一對比就可以看出concurrenthashmap引入并發級別的策略,可以讓concurrenthashmap在多線程環境的性能更加突出,速度也更快。