併發下HashMap錯誤分析

目錄

1.併發add相同hash值or相同key的元素,導致丟失

2. Rehash導致死循環


1.併發add相同hashor相同key的元素,導致丟失

Jdk1.7源碼

void addEntry(int hash, K key, V value, int bucketIndex) {

    if ((size >= threshold) && (null != table[bucketIndex])) {//擴容

        resize(2 * table.length);

        hash = (null != key) ? hash(key) : 0;

        bucketIndex = indexFor(hash, table.length);

    }

    //將新增的元素放到i位置,並將它的next指向舊的元素

    Entry<K,V> e = table[bucketIndex];//當前i位置的元素

    table[bucketIndex] = new Entry<>(hash, key, value, e);

    size++;

}

當A、B兩個線程併發add相同hash值元素時候,當A、B都執行完畢黃色部分後,A、B獲取的e都爲otherValue。

假設A快一步執行綠色部分,此時內存中存儲如下:

下一步,B執行綠色部分,此時內存中存儲如下:

然後就是B線程的值覆蓋了A線程的,導致A線程add的值丟失。

原文:https://blog.csdn.net/lan861698789/article/details/81697398

2. Rehash導致死循環

在擴容時會做一個rehash的操作,這裏會導致死循環。

Jdk1.7 Rehash代碼:

//對老數據進行重新計算hash值,重新做數據下標映射,

//然後全部複製到新數組

void resize(int newCapacity) {

    Entry[] oldTable = table;

    int oldCapacity = oldTable.length;

 

    Entry[] newTable = new Entry[newCapacity];

    transfer(newTable, initHashSeedAsNeeded(newCapacity));

    table = newTable;

    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);

}

void transfer(Entry[] newTable, boolean rehash) {

    int newCapacity = newTable.length;

    for (Entry<K,V> e : table) {

        while(null != e) {

            Entry<K,V> next = e.next;

            if (rehash) {

                e.hash = null == e.key ? 0 : hash(e.key);

            }

            int i = indexFor(e.hash, newCapacity);

            e.next = newTable[i];

            newTable[i] = e;

            e = next;

        }

    }

}

resize步驟如下:

1.對數組table中的元素遍歷

2.對鏈表上的每一個節點遍歷:用 next 取得要轉移那個元素的下一個,將 e 轉移到新 Hash 表的頭部,使用頭插法插入節點。

3.循環2,直到鏈表節點全部轉移

4.循環1,直到所有索引數組全部轉移

經過這幾步,我們會發現轉移的時候是逆序的。假如轉移前鏈表順序是1->2->3,那麼轉移後就會變成3->2->1。這時候就有點頭緒了,死鎖問題不就是因爲1->2的同時2->1造成的嗎?所以,HashMap 的死鎖問題就出在這個transfer()函數上。

先演示單線程

單線程情況下,rehash 不會出現任何問題:

假設hash算法就是最簡單的 key mod table.length(也就是數組的長度)。

最上面的是old hash 表,其中的Hash表的 size = 2, 所以 key = 3, 7, 5,在 mod 2以後碰撞發生在 table[1]

接下來的三個步驟是 Hash表 resize 到4,並將所有的 <key,value> 重新rehash到新 Hash 表的過程

如圖所示:

http://incdn1.b0.upaiyun.com/2016/10/28c8edde3d61a0411511d3b1866f0636.jpg

多線程時候

爲了思路更清晰,我們只將關鍵代碼展示出來

void transfer(Entry[] newTable, boolean rehash) {

    int newCapacity = newTable.length;

    for (Entry<K,V> e : table) {

        while(null != e) {

            Entry<K,V> next = e.next;

            if (rehash) {

                e.hash = null == e.key ? 0 : hash(e.key);

            }

            int i = indexFor(e.hash, newCapacity);

            e.next = newTable[i];

            newTable[i] = e;

            e = next;

        }

    }

}

假設A、B兩個線程都執行resize(),然後都執行完了紅色部分,這時候兩個的e都是key3,next都是key7.

然後A讓出cpu,B全部執行完成,此時內存數據結構如下:

http://incdn1.b0.upaiyun.com/2016/10/665f644e43731ff9db3d341da5c827e1.jpg

此時線程A的key還是key3,next還是key7。(而key7的next卻是key3了)

線程A繼續執行,插入table[i],next指向以前的table[i],也就是key7

內存結構圖如下:

http://incdn1.b0.upaiyun.com/2016/10/011ecee7d295c066ae68d4396215c3d0.jpg

通過圖我們發現,此時進入死循環了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章