ConcurrentHashMap中rehash函式理解
最近看了ConcurrentHashMap的原始碼,對於這個類的整體原理的講解,請參考
探索 ConcurrentHashMap 高併發性的實現機制 這篇文章將ConcurrentHashMap的工作機制已經講得很清楚了,結合原始碼和相關注釋,就可以很好地理解這個類的工作原理了。
這裡補充一下ConcurrentHashMap中rehash函式的執行原理,因為這個地方我看了好長時間才理解是怎麼回事。其實這個函式的註釋也是解釋的比較清楚,但是有些地方只有真正理解了,才能更好地理解註釋說的是什麼。
下面先把這個函式的程式碼和一些我自己的註釋貼出來
void rehash() { HashEntry<K,V>[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity >= MAXIMUM_CAPACITY) return; /* * Reclassify nodes in each list to new Map. 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 traversing table * right now. */ /* * 其實這個註釋已經解釋的很清楚了,主要就是因為擴充套件是按照2的冪次方 * 進行擴充套件的,所以擴充套件前在同一個桶中的元素,現在要麼還是在原來的 * 序號的桶裡,或者就是原來的序號再加上一個2的冪次方,就這兩種選擇。 * 所以原桶裡的元素只有一部分需要移動,其餘的都不要移動。該函式為了 * 提高效率,就是找到最後一個不在原桶序號的元素,那麼連線到該元素後面 * 的子連結串列中的元素的序號都是與找到的這個不在原序號的元素的序號是一樣的 * 那麼就只需要把最後一個不在原序號的元素移到新桶裡,那麼後面跟的一串 * 子元素自然也就連線上了,而且序號還是相同的。在找到的最後一個不在 * 原桶序號的元素之前的元素就需要逐個的去遍歷,加到和原桶序號相同的新桶上 * 或者加到偏移2的冪次方的序號的新桶上。這個都是新建立的元素,因為 * 只能在表頭插入元素。這個原因可以參考 * 《探索 ConcurrentHashMap 高併發性的實現機制》中的講解 */ HashEntry<K,V>[] newTable = HashEntry.newArray(oldCapacity<<1); threshold = (int)(newTable.length * loadFactor); int sizeMask = newTable.length - 1; for (int i = 0; i < oldCapacity ; i++) { // We need to guarantee that any existing reads of old Map can // proceed. So we cannot yet null out each bin. HashEntry<K,V> e = oldTable[i]; if (e != null) { HashEntry<K,V> next = e.next; int idx = e.hash & sizeMask; // Single node on list if (next == null) newTable[idx] = e; else { // Reuse trailing 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; } } // 把最後一個不在原桶序號處的元素賦值到新桶中 // 由於連結串列本身的特性,那麼該元素後面的元素也都能連線過來 // 並且能保證後面的這些元素在新桶中的序號都是和該元素是相等的 // 因為上面的遍歷就是確保了該元素後面的元素的序號都是和這個元素 // 的序號是相等的。不然遍歷中還會重新賦值lastIdx newTable[lastIdx] = lastRun; // Clone all remaining nodes // 這個就是把上面找到的最後一個不在原桶序號處的元素之前的元素賦值到 // 新桶上,注意都是把元素新增到新桶的表頭處 for (HashEntry<K,V> p = e; p != lastRun; p = p.next) { int k = p.hash & sizeMask; HashEntry<K,V> n = newTable[k]; newTable[k] = new HashEntry<K,V>(p.key, p.hash, n, p.value); } } } } table = newTable; }
這個函式裡之前最讓我迷惑的就是那段遍歷找最後一個不在原桶序號處元素的程式碼
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;
當時我主要想不明白的就是newTable[lastIdx] = lastRun;這裡。我剛開始會覺著那麼新桶裡如果這個lastIdx位置已經有元素了,怎麼辦,豈不是就給覆蓋掉了。
後來發現這種情況是不可能出現的。這還是因為table的大小是按照2的冪次方的方式去擴充套件的。
假設原來table的大小是2^k大小,那麼現在新table的大小是2^(k+1)大小。而獲取序號的方式是
int idx = e.hash & sizeMask;
而sizeMask = newTable.length - 1 即sizeMask = 11...1,即全是1,共k個1。獲取序號的演算法是用元素的hash值與sizeMask做與的操作。這樣得到的idx實際上就是元素的hashcode值的低k位的值。而原table的sizeMask也全是1的二進位制,不過總共是k-1位。那麼原table的idx就是元素的hashcode的低k-1位的值。所以說如果元素的hashcode的第k為如果是0,那麼元素在新桶的序號就是和原桶的序號是相等的。如果第k位的值是1,那麼元素在新桶的序號就是原桶的序號+(2^k-1)。所以說只可能是這兩個值。那麼上面的那個newTable[lastIdx] = lastRun;就沒問題了,newTable中新序號處此時肯定是空的。