1. 程式人生 > >hashmap擴容時死迴圈問題

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的冪次方,可以參看這裡寫連結內容 先這樣吧。