最近看了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中新序號處此時肯定是空的。