面試官問:HashMap在併發狀況下爲何形成死循環?一臉懵

這個問題是在面試時常問的幾個問題,通常在問這個問題以前會問Hashmap和HashTable的區別?面試者通常會回答:hashtable是線程安全的,hashmap是線程不安全的。html

那麼面試官就會緊接着問道,爲何hashmap不是線程安全的,會形成什麼問題麼?因而面試者就回答:HashMap在併發狀況下的put操做會形成死循環。java

這時候就會被面試官問:HashMap在併發爲何形成死循環?面試

不少面試者這時候就會一臉懵。沒有過相關經驗和深刻的理解源碼是很難回答這個問題的。安全

下面咱們就經過HahMap源碼來驗證下,多線程併發put操做爲什麼會生成環形鏈表,產生死循環。多線程

這是HashMap擴容的源碼併發

/**
 * Transfers all entries from current table to newTable. 
 */
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;
        } // while  

    }
}

開始以前先回顧一下HashMap的擴容機制: HashMap默認設定的裝載因子爲0.75(可改),HashMap的大小爲length,已經裝載的元素數量爲num,當( num / length )> 裝載因子時, 開始擴容spa

先建立一個散列表HashMap:Map<Integer> map = new HashMap<Integer>(2); ,裝載因子默認0.75,當插入第二個元素時,會發生擴容 咱們先在map中放入六、8兩個元素。線程

<img src="https://upload-images.jianshu.io/upload_images/2710833-7d3073375b3cdc66.png" alt="插入後的狀態" style="zoom: 33%;" />設計

這時有兩個線程都執行put操做,那麼在此刻兩個線程都對HashMap進行擴容,這時候就注意在上文的源碼裏註釋爲(關鍵代碼)這一行:Entry<K,V> next = e.next;code

假如兩個線程分別爲A、B兩個線程。A線程在執行到關鍵代碼這一行線程就被掛起,那麼此刻A線程中:e = 6; next = 8;

接着B線程開始進行擴容,假設新的散列表中,節點6 和 節點8 仍是會產生散列衝突,那麼線程B的擴容過程爲:

  • 先申請一個空間爲舊散列表兩倍大的空間

    <img src="https://upload-images.jianshu.io/upload_images/2710833-091c030487692445.png" alt="申請兩倍大小的空間" style="zoom:33%;" />

  • 將節點6 遷移至新散列表

    <img src="https://upload-images.jianshu.io/upload_images/2710833-fc1de798b8b2cbac.png" alt="節點6遷移至新散列表" style="zoom:33%;" />

  • 將節點8 遷移至新散列表

    <img src="https://upload-images.jianshu.io/upload_images/2710833-0f2c54f633bc98a2.png" alt="將節點8 遷移至新散列表" style="zoom:33%;" />

此時線程B的擴容已經完成,節點8 的後繼節點爲節點6 ,節點6的後繼節點爲null。

咱們將新舊兩個散列表作個對比:

<img src="https://upload-images.jianshu.io/upload_images/2710833-e7602e1bc90df913.png" alt="對比" style="zoom:33%;" />

回顧一下線程A的當前狀態:e = 6; next = 8;,處於掛起狀態。接着A線程取消掛起狀態,接着執行(關鍵代碼)以後的代碼:將e = 6;節點遷移至新的散列表,並將next = 8的節點賦值給e。擴容並遷移節點6後的狀態,以下圖所示:

<img src="https://upload-images.jianshu.io/upload_images/2710833-6afb621e42838e56.png" alt="A線程擴容遷移節點6" style="zoom: 50%;" />

因而第二次執行while循環時,當前待處理節點:e = 8;

在執行(關鍵代碼)這一行時,因爲線程B在擴容時將節點8的後繼節點變爲節點6,因此next不是爲null,而是next = 6;

<img src="https://upload-images.jianshu.io/upload_images/2710833-fb4ac06e60bc55fe.png" alt="dsa" style="zoom: 50%;" />

接着開始執行第三次while循環,因爲節點6的後繼節點爲null,因此 next = null;,執行完第三次while循環的結果爲:

<img src="https://upload-images.jianshu.io/upload_images/2710833-ebda06c0f55e4409.png" alt="321312" style="zoom:50%;" />

循環結束。

能夠看到擴容後的散列表中鏈表成環,若是這時候執行get()方法查詢,就會致使死循環。

總結

HashMap的方法不是線程安全的。HashMap在併發執行put操做時發生擴容,可能會致使節點丟失,產生環形鏈表等狀況。

  • 節點丟失,會致使數據不許
  • 生成環形鏈表,會致使get()方法死循環。

知識拓展

在jdk1.7中,因爲擴容時使用頭插法,在併發時可能會造成環狀列表,致使死循環,在jdk1.8中改成尾插法,能夠避免這種問題,可是依然避免不了節點丟失的問題。

建議

HashMap的設計初衷就不是在併發狀況下使用,若是有併發的場景,推薦使用ConcurrentHashMap

關注公衆號:java之旅

原文出處:https://www.cnblogs.com/chinaxieshuai/p/12433179.html

相關文章
相關標籤/搜索