1. 程式人生 > >HashMap在JDK1.7中可能出現的併發問題

HashMap在JDK1.7中可能出現的併發問題

在JDK1.7及以前中,如果在併發環境中使用HashMap儲存資料,有可能會產生死迴圈的問題,造成cpu的使用率飆升。之所以會發生該問題,實際上就是因為HashMap中的擴容問題。

HashMap的實現實際上是一個數組+連結串列的實現(JDK1.8中當連結串列長度達到一定值會轉化為紅黑樹),當HashMap中儲存的值超過閾值時將會進行一次擴容操作,併發環境下可能存在一個執行緒發現HashMap容量不夠需要擴容,而在這個過程中,另外一個執行緒也剛好進行擴容操作,這時就有可能造成死迴圈的問題。擴容操作一般是在呼叫put(...)方法時進行的,put時會對容量進行檢查。如果在擴容是連結串列中產生一個環形連結串列,那麼在使用get(...)獲取資料時將可能產生死迴圈。

    //進行擴容時呼叫的方法
    void resize(int newCapacity) {
        Entry[] oldTable = table;//儲存舊的陣列
        int oldCapacity = oldTable.length;//儲存舊的容量
        if (oldCapacity == MAXIMUM_CAPACITY) {//無法再進行擴容
            threshold = Integer.MAX_VALUE;
            return;
        }

        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;
            }
        }
    }

在併發環境中,多個執行緒並不是一起執行的,而是由cpu來排程,任何執行緒在任意時刻都有可能停下來,當執行緒停下來時狀態實際上被儲存在棧幀中,下一次被cpu分配到執行權時從讀取當前狀態開始繼續執行,對於被cpu掛起的執行緒在掛起這段時間相當於是"時間暫停"了。明白了這些接下來我們模擬死迴圈出現的情況,實際上這種情況是很不容易出現的,因為需要的條件較多,但是如果我們線上環境執行頻率很高的話產生這種情況就顯得比較容易了,一旦產生一次那就是災難,除了修改程式碼重啟伺服器基本沒別的解決方法。

假設當前HashMap中陣列長度為1,並且只儲存了兩個值(設定的值都是為了產生死迴圈),如下圖,改圖表示一個長度為1的陣列,儲存值為1和3的兩個值:
圖1

現在假設兩個執行緒都在進行擴容操作,執行緒1剛開始,當走到Entry<K,V> next = e.next;時執行緒掛起,cpu被分配給執行緒2。
執行緒2在cpu分配的執行時間中對HashMap操作後變成下圖所示。擴容後,陣列長度變為2,由於擴容是重新插入(頭插法)的原因,值得順序變了,==現在value=3持有value=1的引用==。此時執行緒2掛起,cpu切換到執行緒1。之前儲存的狀態中e的值為value=1的entry且e包含value=3的值得引用。

執行緒1繼續之前的操作:

e.next = newTable[i];
newTable[i] = e;
e = next;

經過兩次迴圈後:

看起來和正常的沒有什麼區別,理論上來說,正常情況下由於value=3的entry的next為null此時應該跳出迴圈,但是問題在於之前的執行緒2使得value=3持有value=1的引用,這時還會在進行一次迴圈:

//此時e = value(1)
Entry<K,V> next = e.next; //next=null
if (rehash) {
    e.hash = null == e.key ? 0 : hash(e.key);
    }
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i]; //e.next=value(3) 死迴圈了
newTable[i] = e;
e = next;

由上可知此時value(3).next=value(1) 並且value(1).next=value(3),產生了環形連結串列。

如果之後呼叫get(...)方法,能找到還好,如果查詢的值不存在,那麼get方法會在環形連結串列處一直迴圈無法退出,只能重啟伺服器了...