標籤:並發 concurrent 高效能 最佳化
深入ConcurrentHashMap講得是如何put元素到ConcurrentHashMap中。
這篇主要分析在put元素的時候,需要擴容這時ConcurrentHashMap是如何做的。
先介紹下ConcurrentHashMap的主要思路,步驟如下:
1.在put的時候,如果需要建立HashEntry結點插入到HashEntry中時,這時候如果滿足擴容條件則進入下面第2步
2.擴容調用的是Segment的rehash方法,只對某個segment擴容。這時需要建立一個segment的HashEntry數組,大小為原來2倍大。
3.然後對原有HashEntry數組中的元素進行重hash,ConcurrentHashMap這裡做了最佳化。
即如果原有的在HashEntry數組中某一個HashEntry鏈,如果裡面的元素,比如有三個元素a->b->c。這裡如果重hash後,a,b,c還是在新數組的相同下標時,可以
想辦法重用這幾個結點。
下面舉例來說明,假設現在一個ConcurrentHashMap,總容量為16,有四個Segment,每個Segment中HashEntry數組大小為4,每個segment的threshold擴容閥值為3。
現在其中一個Segment1的HashEntry儲存情況如下:
這裡可以看到segment1中HashEntry數組已經使用大小為3。其中index0位置的HashEntry因為有hash衝突所以以鏈式來解決衝突,這裡儲存了a,b,c,d,e元素。
index1及index2的hashEntry分別儲存了h1,f1兩個元素。
這裡需要說明的是在做rehash的時候,需要保證盡量不讓reader線程受影響。
rehash的時候會先建立一個大小為原來兩倍的新數組,然後對之前所有元素進行重hash,這個過程中所有的元素重hash後放入新數組位置都是通過建立一個HashEntry來做,以最小化影響之前的reader線程。
這裡假設要put進一個元素到segment1中,key值為p,value為pv。並且這個元素被hash到HashEntry數組的index3位置上,因此需要重建一個HashEntry結點。
這裡會先將當前segment1的count值加1,然後判斷是否該擴容,這裡由於count加1後為4,所以它已經大於上述講到的threshold閥值,因此需要進行擴容。
擴容時會建立一個大小為8的HashEntry數組,然後分別對原有HashEntry數組中的每個元素進行重hash到新數組中。
以分析segment1的index0的HashEntry鏈為例。可以看到有元素: a->b->c->d->e
最簡單粗暴的重hash方法是:
1.首先對a重hash,這裡假設最終a要儲存到新數組位置index4中,因此要建立一個HashEntry用於儲存key a及相應value。它的next指標指向新數組index4上的首個
HashEntry,這裡為null。
2.然後對b進行重hash,假設還是放在新數組位置index4中。這時建立一個新HashEntry,並將它的next指標指向index4的首結點,這裡為a。
這步完成後,index4中元素情況如下:
b->a
3.依次處理完c,d,e。假設c,d,e都還是在新數組index4中。
有沒有發現a,b,c,d,e,還是被分配在新數組同個位置。因此對它們的建立及重新指向是沒有必要的。
事實上ConcurrentHashMap會有一個指標。
在這個指標指向的結點之前都是認為它們儲存在新數組不同位置,因此對於這部分結點還是要建立HashEntry結點及重新調整next指標。
在這個指標結點之後,則被認為是被分配在新數組相同位置,因此它們這部分鏈條結點可以重用。
我們舉例說明,假設a,b,c結點它們最終放在新數組不同位置,而d和e放在新數組相同位置。
因此對於d和e只需做以下動作:
新數組[i]=d
即這裡直接將新數組下標i的引用指向了元素d。
能這樣做的原因是,rehash後由於擴容後新數組大小為原來2倍。因此對於每個元素在新數組的位置,要麼是在原先位置,要麼是原先位置加上原先大小.
這裡d,e在原先的位置0上,擴容後在新數組的位置4上。之後對於原有數組的下標1,2進行rehash時,它們不可能在被放入到新數組的位置0中。
rehash源碼如下:
private void rehash(HashEntry<K,V> node) { /* * Reclassify nodes in each list to new table. Because we * are using power-of-two expansion, the elements from * each bin must either stay at same index, or move with a * power of two offset. We eliminate unnecessary node * creation by catching cases where old nodes can be * reused because their next fields won't change. * Statistically, at the default threshold, only about * one-sixth of them need cloning when a table * doubles. The nodes they replace will be garbage * collectable as soon as they are no longer referenced by * any reader thread that may be in the midst of * concurrently traversing table. Entry accesses use plain * array indexing because they are followed by volatile * table write. */ HashEntry<K,V>[] oldTable = table; int oldCapacity = oldTable.length; int newCapacity = oldCapacity << 1; threshold = (int)(newCapacity * loadFactor); HashEntry<K,V>[] newTable = (HashEntry<K,V>[]) new HashEntry[newCapacity]; int sizeMask = newCapacity - 1; for (int i = 0; i < oldCapacity ; i++) { HashEntry<K,V> e = oldTable[i]; if (e != null) { HashEntry<K,V> next = e.next; int idx = e.hash & sizeMask; if (next == null) // Single node on list newTable[idx] = e; else { // Reuse consecutive sequence at same slot HashEntry<K,V> lastRun = e; int lastIdx = idx; for (HashEntry<K,V> last = next; last != null; last = last.next) { int k = last.hash & sizeMask; if (k != lastIdx) { lastIdx = k; lastRun = last; } } newTable[lastIdx] = lastRun; // Clone remaining nodes for (HashEntry<K,V> p = e; p != lastRun; p = p.next) { V v = p.value; int h = p.hash; int k = h & sizeMask; HashEntry<K,V> n = newTable[k]; newTable[k] = new HashEntry<K,V>(h, p.key, v, n); } } } } int nodeIndex = node.hash & sizeMask; // add the new node node.setNext(newTable[nodeIndex]); newTable[nodeIndex] = node; table = newTable; }