HashMap中的resize以及死鏈的情況


之前我已經寫過關於HashMap的內容了:http://www.cnblogs.com/wang-meng/p/7545725.html
我們都知道HashMap是線程不安全的, 如果多線程來訪問會有什么問題呢? 答案是會造成死鎖。
接下來我們就分析下為何會造成死鎖。

說到HashMap中死鎖的情況, 我們就必須要先講下resize()方法, 顧名思義, 這個方法就是來擴容的。
當HashMap的size超過 thredshold時, 就需要擴容了。 當我們put時:
(截圖代碼為JDK7 HashMap源碼)
Image
 
首先,我們需要知道幾個最基本的概念: Entry<K,V>[] table的初始化長度length(默認值是16),Load factor為負載因子(默認值是0.75),threshold是HashMap所能容納的最大數據量的Entry(鍵值對)個數。size是HashMap中實際存在 的鍵值對數量。threshold = length * Load factor。也就是說,在數組定義好長度之后,負載因子越大,所能容納的鍵值對個數越多。
 
接着我們直接看上面的代碼, 當size >= threshold且table[bucketIndex]不為空就會觸發resize操作。 然后看resize()方法:
Image(1)
 
這里重點就是transfer方法, 接着我們來看transfer方法:
Image(2)
 
第一: 遍歷舊的table
第二: 將舊的table中每個元素重新計算hash值, 然后賦予新的table中
具體我們將用圖示的方法來解析:
 
單線程擴容:
假設:hash算法就是簡單的key與length(數組長度)求余。
          hash表長度為2,如果不擴容, 那么元素key為3,5,7按照計算(key%table.length)的話都應該碰撞到table[1]上
         
擴容:hash表長度會擴容為4
          重新hash,key=3 會落到table[3]上(3%4=3), 當前e.next為key(7), 繼續while循環
          重新hash,key=7 會落到table[3]上(7%4=3), 產生碰撞, 這里采用的是頭插入法,所以key=7的Entry會排在key=3前面(這里可以具體看while語句中代碼)
          當前e.next為key(5), 繼續while循環
          重新hash,key=5 會落到table[1]上(5%4=1), 當前e.next為null, 跳出while循環, resize結束
         
 
如題如圖所示:
 
Image(3)
 
 
多線程擴容:
這里我們先把核心代碼搬出來, 方便查看

while(null != e) {
    Entry<K,V> next = e.next; //第一行
    int i = indexFor(e.hash, newCapacity); //第二行
    e.next = newTable[i]; //第三行
    newTable[i] = e; //第四行
    e = next; //第五行
}

去掉了一些冗余的代碼, 層次結構更加清晰了。
第一行:記錄odl hash表中e.next
第二行:rehash計算出數組的位置(hash表中桶的位置)
第三行:e要插入鏈表的頭部, 所以要先將e.next指向new hash表中的第一個元素
第四行:將e放入到new hash表的頭部
第五行: 轉移e到下一個節點, 繼續循環下去

核心代碼如上所說, 下面就是多線程同時put的情況了, 然后同時進入transfer方法中:

假設這里有兩個線程同時執行了put()操作,並進入了transfer()環節

while(null != e) { Entry<K,V> next = e.next; //線程1執行到這里被調度掛起了  e.next = newTable[i]; newTable[i] = e; e = next; }

那么現在的狀態為:

從上面的圖我們可以看到,因為線程1的 e 指向了 key(3),而 next 指向了 key(7),在線程2 rehash 后,就指向了線程2 rehash 后的鏈表。

然后線程1被喚醒了:

  1. 執行e.next = newTable[i],於是 key(3)的 next 指向了線程1的新 Hash 表,因為新 Hash 表為空,所以e.next = null
  2. 執行newTable[i] = e,所以線程1的新 Hash 表第一個元素指向了線程2新 Hash 表的 key(3)。好了,e 處理完畢。
  3. 執行e = next,將 e 指向 next,所以新的 e 是 key(7)

然后該執行 key(3)的 next 節點 key(7)了:

  1. 現在的 e 節點是 key(7),首先執行Entry<K,V> next = e.next,那么 next 就是 key(3)了
  2. 執行e.next = newTable[i],於是key(7) 的 next 就成了 key(3)
  3. 執行newTable[i] = e,那么線程1的新 Hash 表第一個元素變成了 key(7)
  4. 執行e = next,將 e 指向 next,所以新的 e 是 key(3)

這時候的狀態圖為:

然后又該執行 key(7)的 next 節點 key(3)了:

  1. 現在的 e 節點是 key(3),首先執行Entry<K,V> next = e.next,那么 next 就是 null
  2. 執行e.next = newTable[i],於是key(3) 的 next 就成了 key(7)
  3. 執行newTable[i] = e,那么線程1的新 Hash 表第一個元素變成了 key(3)
  4. 執行e = next,將 e 指向 next,所以新的 e 是 key(7)

這時候的狀態如圖所示:

 
很明顯,環形鏈表出現了!!當然,現在還沒有事情,因為下一個節點是 null,所以transfer()就完成了,等put()的其余過程搞定后,HashMap 的底層實現就是線程1的新 Hash 表了。

 


免責聲明!

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



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