標籤:並發 concurrent 線程 高效能 map
ConcurrentHashMap能夠做到比較高效能的並發訪問。
ConcurrentHashMap內部有一個Segment數組,每個Segment有一個lock。
Segment相當於是一個子map,擁有一個HashEntity數組。
這樣可以將並發壓力分攤到多個Segment上。
ConcurrentHashMap組成圖:
這裡的Segment個數必須為2的n次方,為了之後高效計算要存放的key存放在哪個Segment上(用 << 實現)。
Segment內部擁有一個類型為volatile的HashEntry數組,這是為了能夠讓其它線程看到最新的值。
ConcurrentHashMap擁有put,get,remove等操作。而原有的擴容操作,為了效能則只針對單個Segment來進行。
關鍵的Segment相當了ConcurrentHashMap中的子map,它包含了一個HashEntry數組。它的類圖如下:
這裡關鍵的Segment類圖如下:
HashEntry內部儲存了hash值,final的key值,及volatile的value及next值。
這裡value及next值是volatile是為實現get操作的不需要加鎖提供了基礎。
HashEntry類圖如下:
下面分別從ConcurrentHashMap建立,往ConcurrentHashMap放入元素,從ConcurrentHashMap取出元素來進行分析。
一.ConcurrentHashMap建立
在用ConcurrentHashMap時,可以在建構函式中傳入你想要的map容量,loadFactor裝載因子,並發數(大致決定了有幾個Segment)。
如果不傳入,使用空建構函式時,預設的map容量大小為16,loadFactor為0.75,並發數為16。
我們以預設的參數開始分析。
首先需要確定總容量大小(這裡指所有Segment中存放數組元素的數組大小總和),Segment總數,以及每個Segment中HashEntry數組的大小。
我們先附上代碼:
public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) { if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0) throw new IllegalArgumentException(); if (concurrencyLevel > MAX_SEGMENTS) concurrencyLevel = MAX_SEGMENTS; // Find power-of-two sizes best matching arguments int sshift = 0; int ssize = 1; while (ssize < concurrencyLevel) { ++sshift; ssize <<= 1; } this.segmentShift = 32 - sshift; this.segmentMask = ssize - 1; if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; int c = initialCapacity / ssize; if (c * ssize < initialCapacity) ++c; int cap = MIN_SEGMENT_TABLE_CAPACITY; while (cap < c) cap <<= 1; // create segments and segments[0] Segment<K,V> s0 = new Segment<K,V>(loadFactor, (int)(cap * loadFactor), (HashEntry<K,V>[])new HashEntry[cap]); Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize]; UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0] this.segments = ss; }
可以看到,對於Segment個數ssize取的是以concurrencyLevel為上界的2的n次方,這裡由於concurrencyLevel為16,所以ssize也為16。
然後計算segmentShift,這個值用於後續在存放元素到ConcurrentHashMap時,用於決定有多少位hash高位參與計算存放元素的Segment數組下標。
這裡segmentShift為32-sshift。sshift取的是Segment個數ssize的2的n次數的n值,即在計算ssize時左移的次數,這裡為4。
所以segmentShift為28。
接著計算出segmentMask,這個值用於在計算得到存放元素的Segment所在數組的下標,這裡為了效率使用 & 操作來替代%模操作。
這裡由於Segment個數為16,所以segmentMask為15。
之後會計算出每個Segment的數組容量,這裡先計算出每個Segment的大致大小,即用int c = initialCapacity / ssize; 來得出c,這裡c為1。
之後會初始化一個cap作為真正的segment中HashEntry數組大小,將它初始化為2。
然後為了保證得出的segMent中HashEntry的大小為2的n次方,所以後續會對cap做以下操作:
while (cap < c)
cap <<= 1;
即cap的值要不是2(c值小於等於2時),或者是大於2的2的n次方。
得到最終每個Segment中HashEntry數組大小後,建立一個Segment數組,並在Segment數組0處初始化建立一個Segment。
最後將ConcurrentHashMap的segments設定為新建立的Segments數組。
這裡可以看到最後將建立的Segment s0是調用UNSAFE.putOrderedObject(ss,SBASE,s0)來將其放入到ss數組中。
這裡的UNSAFE.putOrderedObject是JAVA提供的用於直接操作記憶體的方法,其中參數ss是數組。
UNSAFE.putOrderedObject會延遲更新到記憶體中,但是由於後續在擷取segment數組中的segment時,採用的是UNSAFE.getObjectVolatile,所以能夠保證對於segment放入數組的操作對於後續的線程是可見的。
SBASE是ConcurrentHashMap的一個靜態final long的值,相當於是Segment數組的首地址。
s0則是建立的需要放入ss數組中的segment執行個體。這裡是將s0放入到ss數組位置0中。
如果提供類似:UNSAFE.putOrderedObject(ss,SBASE+offset,s)表明將元素s放入到數組ss的SBASE+offset位置。
其中這裡的offset:
offset=步長*要放在數組第幾位
步長一般是在編譯期已經確定,這裡測試過對於:Double或者Integer等,步長都是4。