ConcurrentHashMap
ConcurrentHashMap是一個安全執行緒的Hash Table,它的主要功能是提供了一組和HashTable功能相同但是安全執行緒的方法。ConcurrentHashMap可以做到讀取資料不加鎖,並且其內部的結構可以讓其在進行寫操作的時候能夠將鎖的粒度保持地盡量地小,不用對整個ConcurrentHashMap加鎖。 ConcurrentHashMap的內部結構
ConcurrentHashMap為了提高本身的並發能力,在內部採用了一個叫做Segment的結構,一個Segment其實就是一個類Hash Table的結構,Segment內部維護了一個鏈表數組,我們用下面這一幅圖來看下ConcurrentHashMap的內部結構:
從上面的結構我們可以瞭解到,ConcurrentHashMap定位一個元素的過程需要進行兩次Hash操作,第一次Hash定位到Segment,第二次Hash定位到元素所在的鏈表的頭部,因此,這一種結構的帶來的副作用是Hash的過程要比普通的HashMap要長,但是帶來的好處是寫操作的時候可以只對元素所在的Segment進行加鎖即可,不會影響到其他的Segment,這樣,在最理想的情況下,ConcurrentHashMap可以最高同時支援Segment數量大小的寫操作(剛好這些寫操作都非常平均地分布在所有的Segment上),所以,通過這一種結構,ConcurrentHashMap的並發能力可以大大的提高。 Segment
我們再來具體瞭解一下Segment的資料結構:
| 1 |
static finalclassSegment<k,v> extendsReentrantLockimplements Serializable { |
| 2 |
transientvolatileint count; |
| 5 |
transientvolatileHashEntry<k,v>[] table; |
詳細解釋一下Segment裡面的成員變數的意義: count:Segment中元素的數量 modCount:對table的大小造成影響的操作的數量(比如put或者remove操作) threshold:閾值,Segment裡面元素的數量超過這個值依舊就會對Segment進行擴容 table:鏈表數組,數組中的每一個元素代表了一個鏈表的頭部 loadFactor:負載因子,用於確定threshold HashEntry
Segment中的元素是以HashEntry的形式存放在鏈表數組中的,看一下HashEntry的結構:
| 1 |
static finalclassHashEntry<k,v> { |
| 5 |
finalHashEntry<k,v> next; |
可以看到HashEntry的一個特點,除了value以外,其他的幾個變數都是final的,這樣做是為了防止鏈表結構被破壞,出現ConcurrentModification的情況。 ConcurrentHashMap的初始化
下面我們來結合原始碼來具體分析一下ConcurrentHashMap的實現,先看下初始化方法:
| 01 |
public ConcurrentHashMap(intinitialCapacity, |
| 02 |
floatloadFactor,int concurrencyLevel) { |
| 03 |
if(!(loadFactor >0) || initialCapacity <0|| concurrencyLevel <= 0) |
| 04 |
thrownewIllegalArgumentException(); |
| 06 |
if(concurrencyLevel > MAX_SEGMENTS) |
| 07 |
concurrencyLevel = MAX_SEGMENTS; |
| 09 |
// Find power-of-two sizes best matching arguments |
| 12 |
while(ssize < concurrencyLevel) { |
| 16 |
segmentShift =32- sshift; |
| 17 |
segmentMask = ssize -1; |
| 18 |
this.segments = Segment.newArray(ssize); |
| 20 |
if(initialCapacity > MAXIMUM_CAPACITY) |
| 21 |
initialCapacity = MAXIMUM_CAPACITY; |
| 22 |
intc = initialCapacity / ssize; |
| 23 |
if(c * ssize < initialCapacity) |
| 29 |
for(inti = 0; i <this.segments.length; ++i) |
| 30 |
this.segments[i] =newSegment<k,v>(cap, loadFactor); |
CurrentHashMap的初始化一共有三個參數,一個initialCapacity,表示初始的容量,一個loadFactor,表示負載參數,最後一個是concurrentLevel,代表ConcurrentHashMap內部的Segment的數量,ConcurrentLevel一經指定,不可改變,後續如果ConcurrentHashMap的元素數量增加導致ConrruentHashMap需要擴容,ConcurrentHashMap不會增加Segment的數量,而只會增加Segment中鏈表數組的容量大小,這樣的好處是擴容過程不需要對整個ConcurrentHashMap做rehash,而只需要對Segment裡面的元素做一次rehash就可以了。
整個ConcurrentHashMap的初始化方法還是非常簡單的,先是根據concurrentLevel來new出Segment,這裡Segment的數量是不大於concurrentLevel的最大的2的指數,就是說Segment的數量永遠是2的指數個,這樣的好處是方便採用移位操作來進行hash,加快hash的過程。接下來就是根據intialCapacity確定Segment的容量的大小,每一個Segment的容量大小也是2的指數,同樣使為了加快hash的過程。
這邊需要特別注意一下兩個變數,分別是segmentShift和segmentMask,這兩個變數在後面將會起到很大的作用,假設建構函式確定了Segment的數量是2的n次方,那麼segmentShift就等於32減去n,而segmentMask就等於2的n次方減一。 ConcurrentHashMap的get操作
前面提到過ConcurrentHashMap的get操作是不用加鎖的,我們這裡看一下其實現:
| 1 |
public V get(Object key) { |
| 2 |
inthash = hash(key.hashCode()); |
| 3 |
returnsegmentFor(hash).get(key, hash); |
看第三行,segmentFor這個函數用於確定操作應該在哪一個segment中進行,幾乎對ConcurrentHashMap的所有操作都需要用到這個函數,我們看下這個函數的實現:
| 1 |
final Segment<k,v> segmentFor(inthash) { |
| 2 |
returnsegments[(hash >>> segmentShift) & segmentMask]; |
這個函數用了位操作來確定Segment,根據傳入的hash值向右無符號右移segmentShift位,然後和segmentMask進行與操作,結合我們之前說的segmentShift和segmentMask的值,就可以得出以下結論:假設Segment的數量是2的n次方,根據元素的hash值的高n位就可以確定元素到底在哪一個Segment中。
在確定了需要在哪一個segment中進行操作以後,接下來的事情就是調用對應的Segment的get方法:
| 01 |
V get(Object key, inthash) { |
| 02 |
if(count !=0) {// read-volatile |
| 03 |
HashEntry<k,v> e = getFirst(hash); |
| 05 |
if(e.hash == hash && key.equals(e.key)) { |
| 09 |
returnreadValueUnderLock(e);// recheck |
先看第二行代碼,這裡對count進行了一次判斷,其中count表示Segment中元素的數量,我們可以來看一下count的定義:
| 1 |
transient volatile int count; |
可以看到count是volatile的,實際上這裡裡面利用了volatile的語義: 對volatile欄位的寫入操作happens-before於每一個後續的同一個欄位的讀操作。
因為實際上put、remove等操作也會更新count的值,所以當競爭發生的時候,volatile的語義可以保證寫操作在讀操作之前,也就保證了寫操作對後續的讀操作都是可見的,這樣後面get的後續操作就可以拿到完整的元素內容。
然後,在第三行,調用了getFirst()來取得鏈表的頭部:
| 1 |
HashEntry<k,v> getFirst(inthash) { |
| 2 |
HashEntry<k,v>[] tab = table; |
| 3 |
returntab[hash & (tab.length -1)]; |
同樣,這裡也是用位操作來確定鏈表的頭部,hash值和HashTable的長度減一做與操作,最後的結果就是hash值的低n位,其中n是HashTable的長度以2為底的結果。
在確定了鏈表的頭部以後,就可以對整個鏈表進行遍曆,看第4行,取出key對應的value的值,如果拿出的value的值是null,則可能這個key,value對正在put的過程中,如果出現這種情況,那麼就加鎖來保證取出的value是完整的,如果不是null,則直接返回value。 ConcurrentHashMap的put操作
看完了get操作,再看下put操作,put操作的前面也是確定Segment的過程,這裡不再贅述,直接看關鍵的segment的put方法:
| 01 |
V put(K key, inthash, V value,boolean onlyIfAbsent) { |
| 05 |
if(c++ > threshold)// ensure capacity |
| 07 |
HashEntry<k,v>[] tab = table; |