hashmap擴容時死迴圈問題
廢話不多說,大家都知道,hashmap不能用於多執行緒場景中,多執行緒下推薦使用concurrentHashmap! 但為什麼多執行緒下不能使用hashmap那,主要原因就在於其的擴容機制。
文章是綜合他人部落格,自己加點寫成的。(such as 我沒畫圖,網上找的圖。。) 故事的起源從hashmap的資料存放開始說起,預設hashmap大小是16.當資料過大時,毫無疑問,hashmap需要擴容去支援存放更多的資料。 原始碼如下 ——–Put一個Key,Value對到Hash表中:
public V put(K key, V value)
{
......
//計算Hash值
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
//各種校驗吧
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//該key不存在,需要增加一個結點
addEntry(hash, key, value, i);
return null;
}
這裡新增一個節點需要檢查是否超出容量,出現了一個負載因子。
void addEntry(int hash, K key, V value, int bucketIndex)
{
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
//檢視當前的size是否超過了我們設定的閾值threshold,如果超過,需要resize
if (size++ >= threshold)
resize(2 * table.length);//擴容都是2倍2倍的來的,
}
至於為什麼擴容都是2的冪次方這個問題,建議自行搜尋。。
既然新建了一個更大尺寸的hash表,然後把資料從老的Hash表中遷移到新的Hash表中。
void resize(int newCapacity)
{
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
......
//建立一個新的Hash Table
Entry[] newTable = new Entry[newCapacity];
//將Old Hash Table上的資料遷移到New Hash Table上
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
好,重點在這裡面的transfer()!
void transfer(Entry[] newTable)
{
Entry[] src = table;
int newCapacity = newTable.length;
//下面這段程式碼的意思是:
// 從OldTable裡摘一個元素出來,然後放到NewTable中
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
do迴圈裡面的是最能說明問題的 當只有一個執行緒的時候: 圖是網上找的,話我就湊合著說了,圖上的hash演算法是自定義的,不要糾結這個,是簡單的用key mod 一下表的大小(也就是陣列的長度)。不是那個實際的hash演算法! 最上面的是old hash 表,其中的Hash表的size=2, 所以key = 3, 7, 5,在mod 2以後都衝突在table[1]這裡了。 接下來的三個步驟是Hash表 擴容變成4,然後所有的
do {
Entry<K,V> next = e.next; // <--假設執行緒一執行到這裡就被排程掛起了
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
而我們的執行緒二執行完成了。於是我們有下面的這個樣子。
注意,因為Thread1的 e 指向了key(3),而next指向了key(7),其線上程二rehash後,指向了執行緒二重組後的連結串列。我們可以看到連結串列的順序被反轉後。 這裡的意思是執行緒1這會還沒有完全開始擴容,但e和next已經指向了,執行緒2是正常的擴容的,那這會在3這個位置上,就是7->3這個順序。 然後: 2)執行緒一被排程回來執行。
先是執行 newTalbe[i] = e; 然後是e = next,導致了e指向了key(7), 而下一次迴圈的next = e.next導致了next指向了key(3) 注意看圖裡面的線,執行緒1指向執行緒2裡面的key3. 回到執行緒1裡面的時候 3)一切安好。
執行緒一接著工作。把key(7)摘下來,放到newTable[i]的第一個,然後把e和next往下移。 這時候,原來的執行緒2裡面的key7的e和key3的next沒了,e=key3,next=null。
4)環形連結出現。 當繼續執行,需要將key3加回到key7的前面。 e.next = newTable[i] 導致 key(3).next 指向了 key(7)
注意:此時的key(7).next 已經指向了key(3), 環形連結串列就這樣出現了。 我理解是執行緒2生成的e和next的關係影響到了執行緒1裡面的情況。從而打亂了正常的e和next的鏈。
於是,當我們的執行緒一呼叫到,HashTable.get(11)時,即又到了3這個位置,需要插入新的,那這會就e 和next就亂了。
恩,至於上面那個為什麼擴容都要是2的冪次方,可以參看這裡寫連結內容 先這樣吧。