ConcurrentHashMap 擴容分析拾遺


前言

這是一篇對 transfer 方法的拾遺,關於之前那篇文章的一些一筆帶過,或者當時不知道的地方進行回顧。

疑點 1. 為什么將鏈表拆成兩份的時候,0 在低位,1 在高位?

回顧一下 transfer 的相關代碼:

int runBit = fh & n;
Node<K,V> lastRun = f; 
for (Node<K,V> p = f.next; p != null; p = p.next) {
    // 取於桶中每個節點的 hash 值
    int b = p.hash & n;
    if (b != runBit) {
        runBit = b; 
        lastRun = p; 
    }
}
if (runBit == 0) {// 如果最后更新的 runBit 是 0 ,設置低位節點
    ln = lastRun;
    hn = null;
}
else {
    hn = lastRun; // 如果最后更新的 runBit 是 1, 設置高位節點
    ln = null;
}
for (Node<K,V> p = f; p != lastRun; p = p.next) {
    int ph = p.hash; K pk = p.key; V pv = p.val;
    // 如果與運算結果是 0,那么就還在低位
    if ((ph & n) == 0) // 如果是0 ,那么創建低位節點
        ln = new Node<K,V>(ph, pk, pv, ln);
    else // 1 則創建高位
        hn = new Node<K,V>(ph, pk, pv, hn);
}

關鍵看上面注釋的代碼,如果 runBit 是 0,那么就設置在低位節點,反之,如果是 1,設置在高位。

為什么這么設計呢?當時樓主一筆帶過,稱之為這個貌似沒有什么特殊含義,實在是愚蠢之極。

今天解釋一下。

這要從 ConcurrentHashMap 的取於下標算法開始說起。

我們知道,在 putVal 方法中,會通過取於對象的 hash 值獲取下標。具體代碼如下:

 else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {

也就是 (n - 1) & hash),這個 n 就是 length。這個其實相當於 hash % n(n 必須是2的指數)。但是比 % 更高效。

復習一下與運算:第一個操作數的的第n位於第二個操作數的第n位如果都是1,那么結果的第n為也為1,否則為0.

然后開始推導:

(n - 1) & hash),取於算法。
假設,我們的 table 長度是 16,也就是 10000,減一就是 01111. 取於下面這個數。這個數特別之處在於,
他的右起第 5 位是 0。如果是 10000 & 這個數,結果是 0.
000000001111                     000000010000 
010101001001 // 結果 9            010101001001 // &運算結果: 0

當我們擴容后,16 變成 32,也就是 10000. 再看看 (n - 1) & hash) 的結果:

000000011111                    
010101001001 // 結果還是 9

從這里可以看出,如果 & 運算是 0 ,那么即使擴容,下標也是不變的。

再看看另一種情況,換一個 hash 數字,右起第五位是 1 :           
000000001111                      000000010000
010101010001 // 結果 1             010101010001 // &運算結果: 1

這里的 & 與運算后,結果是 1,和上面的不同。同時, (n - 1) & hash) 的結果也是 1.

當擴容后,結果是什么樣子呢?
000000011111
010101010001 // 結果變化:10001 == 17

可以看到,(n - 1) & hash) 的結果是 17,17 - 1,剛好是 16,而這個 16 的原因是我們的二進制進了一位。

現在明白了吧?0 在低位,1 在高位不是隨便設計的。這里讓我想到了一致性 hash 算法:當桶的數量變化了,那么 hash 的位置也會變化

這里的設計是為了防止下次取值的時候,hash 不到正確的位置。

實際上,JDK 1.8 的 HashMap 也是這么實現的重新散列。文章深入理解 HashMap put 方法(JDK 8逐行剖析)。其中 resize 方法和這里高度類似。

疑點 2:為什么會有 i >= n || i + n >= nextn 的判斷?

回顧一下代碼:

if (i < 0 || i >= n || i + n >= nextn) {
    int sc;
    if (finishing) { 
        nextTable = null;
        table = nextTab;
        sizeCtl = (n << 1) - (n >>> 1); 
        return;
    }

這個判斷在當時看來是沒有可能存在的。到現在也沒明白為什么。。。。

如果有大佬知道,請指點一二。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM