1. 程式人生 > >不正當使用HashMap導致cpu 100%的問題追究

不正當使用HashMap導致cpu 100%的問題追究

因最近hashmap誤用引起的死迴圈又發生了一些案例,左耳朵浩子寫了一篇blog 疫苗:Java HashMap的死迴圈,看了一下,大家的分析如出一轍。這篇blog也是好幾年前寫的了,之前在平臺技術部的部落格上貼過,隨著組織結構的調整,那個部落格可能不再維護,把這篇文章在這兒也儲存一下。

李鵬同學在blog裡寫了篇關於HashMap死鎖模擬的文章: http://blog.csdn.net/madding/archive/2010/08/25/5838477.aspx 做個糾正,那個不是死鎖問題,而是死迴圈。

這個問題,我們以前討論過。 校長之前的部落格和淘寶的畢玄的《分散式Java應用:基礎與實踐》一書中都提到過 velocity導致cpu 100% 的bug,起因是HashMap的使用不當所致。

在之前的郵件列表裡,校長提出過這個問題,當時我沒仔細看,不清楚這個問題究竟是對 HashMap的誤用,還是HashMap的潛在問題, 當時感覺不太可能是HashMap自身的問題,否則問題大了。應該是屬於在併發的場景下錯誤的 使用了HashMap。昨天看了李鵬的blog後,覺得這個事情還是應該搞清楚一下;雖然我推測是連結串列形成閉環,但 沒有去證明過。從網上找了一下: http://blog.csdn.net/autoinspired/archive/2008/07/16/2662290.aspx 裡面也有提到:

產生這個死迴圈的根源在於對一個未保護的共享變數 — 一個”HashMap”資料結構的操作。當在 所有操作的方法上加了”synchronized”後,一切恢復了正常。檢查”HashMap”(Java SE 5.0)的源 碼,我們發現有潛在的破壞其內部結構最終造成死迴圈的可能。在下面的程式碼中,如果我們使得 HashMap中的entries進入迴圈,那 麼”e.next()”永遠都不會為null。

不僅get()方法會這樣,put()以及其他對外暴露的方法都會有這個風險,這算jvm的bug嗎?應該說不是的,這個現象很早以前就報告出來了(詳細見: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6423457)。Sun的工程師並不認為這 是bug,而是建議在這樣的場景下應用”ConcurrentHashMap”,在構建可擴充套件的系統時應將這點 納入規範中。

這篇翻譯提到了對HashMap的誤用,但它沒有點破HashMap內部結構在什麼樣誤用情況下怎麼被 破壞的;我想要一個有力的場景來弄清楚。再從李鵬的blog來看,用了2個執行緒來put就模擬出來了,最後堆疊是在 transfer

方法上(該方法是資料擴容時將資料從舊容器轉移到新容器)。 仔細分析了一下里面的程式碼,基本得出了原因,證明了我之前的推測。

假設擴容時的一個場景如下(右邊的容器是一個長度 2 倍於當前容器的陣列) 單執行緒情況。

我們分析資料轉移的過程,主要是連結串列的轉移。

執行過一次後的狀態:

最終的結果:

兩個執行緒併發情況下,擴容時可能會創建出 2 個新陣列容器

順利的話,最終轉移完可能是這樣的結果

但併發情況下,出現死迴圈的可能場景是什麼呢? 還要詳細的分析一下程式碼,下面的程式碼中重點在 do/while 迴圈結構中(完成鏈 表的轉移)。

// 擴容操作,從一個數組轉移到另一個數組
void transfer(Entry[] newTable) { 
    Entry[] src = table;
    int newCapacity = newTable.length; 
    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); // 可能導致死迴圈
        }
    }
}

2 個執行緒併發情況下, 當執行緒 1 執行到上面第 9 行時,而執行緒 2 已經完成了一 輪 do/while 操作,那麼它的狀態如下圖:
(上面的陣列時執行緒 1 的,已經完成了連結串列資料的轉移;下面的是執行緒 2 的,它 即將開始進行對連結串列資料的轉移,此時它記錄 E1 和 E2 的首位已經被執行緒 1 翻 轉了)

後續的步驟如下:

1) 插入 E1 節點,E1 節點的 next 指向新容器索引位置上的值(null 或 entry)

2) 插入 E2 節點,E2 的 next 指向當前索引位置上的引用值 E1

3)因為 next 不為 null,連結串列繼續移動,此時 2 節點之間形成了閉環。造成了 死迴圈。

上面只是一種情況,造成單執行緒死迴圈,雙核 cpu 的話佔用率是 50%,還有導致 100%的情況,應該也都是連結串列的閉環所致。

最終,這並不是 HashMap 的問題,是使用場景的不當,在併發情況下選擇非執行緒 安全的容器是沒有保障的。