面試官問:HashMap在并發(fā)情況下為什么造成死循環(huán)?一臉懵

這個問題是在面試時常問的幾個問題,一般在問這個問題之前會問Hashmap和HashTable的區(qū)別?面試者一般會回答:hashtable是線程安全的,hashmap是線程不安全的。

那么面試官就會緊接著問道,為什么hashmap不是線程安全的,會造成什么問題么?于是面試者就回答:HashMap在并發(fā)情況下的put操作會造成死循環(huán)。

這時候就會被面試官問:HashMap在并發(fā)為什么造成死循環(huán)?

很多面試者這時候就會一臉懵。沒有過相關(guān)經(jīng)驗和深入的理解源碼是很難回答這個問題的。

下面我們就通過HahMap源碼來驗證下,多線程并發(fā)put操作為何會生成環(huán)形鏈表,產(chǎn)生死循環(huán)。

這是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) {
            //(關(guān)鍵代碼)
            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默認設(shè)定的裝載因子為0.75(可改),HashMap的大小為length,已經(jīng)裝載的元素數(shù)量為num,當(dāng)( num / length )> 裝載因子時,
開始擴容

先創(chuàng)建一個散列表HashMap:Map<Integer> map = new HashMap<Integer>(2);,裝載因子默認0.75,當(dāng)插入第二個元素時,會發(fā)生擴容
我們先在map中放入6、8兩個元素。

插入后的狀態(tài)

這時有兩個線程都執(zhí)行put操作,那么在此刻兩個線程都對HashMap進行擴容,這時候就注意在上文的源碼里注釋為(關(guān)鍵代碼)這一行:Entry<K,V> next = e.next;

假如兩個線程分別為A、B兩個線程。A線程在執(zhí)行到關(guān)鍵代碼這一行線程就被掛起,那么此刻A線程中:e = 6; next = 8;

接著B線程開始進行擴容,假設(shè)新的散列表中,節(jié)點6 和 節(jié)點8 還是會產(chǎn)生散列沖突,那么線程B的擴容過程為:

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

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

  • 將節(jié)點6 遷移至新散列表

節(jié)點6遷移至新散列表
  • 將節(jié)點8 遷移至新散列表
將節(jié)點8 遷移至新散列表

此時線程B的擴容已經(jīng)完成,節(jié)點8 的后繼節(jié)點為節(jié)點6 ,節(jié)點6的后繼節(jié)點為null。

我們將新舊兩個散列表做個對比:

對比

回顧一下線程A的當(dāng)前狀態(tài):e = 6; next = 8;,處于掛起狀態(tài)。接著A線程取消掛起狀態(tài),接著執(zhí)行(關(guān)鍵代碼)之后的代碼:將e = 6;節(jié)點遷移至新的散列表,并將next = 8的節(jié)點賦值給e。擴容并遷移節(jié)點6后的狀態(tài),如下圖所示:

A線程擴容遷移節(jié)點6

于是第二次執(zhí)行while循環(huán)時,當(dāng)前待處理節(jié)點:e = 8;

在執(zhí)行(關(guān)鍵代碼)這一行時,由于線程B在擴容時將節(jié)點8的后繼節(jié)點變?yōu)楣?jié)點6,所以next不是為null,而是next = 6;

接著開始執(zhí)行第三次while循環(huán),由于節(jié)點6的后繼節(jié)點為null,所以 next = null;,執(zhí)行完第三次while循環(huán)的結(jié)果為:

循環(huán)結(jié)束。

可以看到擴容后的散列表中鏈表成環(huán),如果這時候執(zhí)行get()方法查詢,就會導(dǎo)致死循環(huán)。

總結(jié)

HashMap的方法不是線程安全的。HashMap在并發(fā)執(zhí)行put操作時發(fā)生擴容,可能會導(dǎo)致節(jié)點丟失,產(chǎn)生環(huán)形鏈表等情況。

  • 節(jié)點丟失,會導(dǎo)致數(shù)據(jù)不準
  • 生成環(huán)形鏈表,會導(dǎo)致get()方法死循環(huán)。

知識拓展

在jdk1.7中,由于擴容時使用頭插法,在并發(fā)時可能會形成環(huán)狀列表,導(dǎo)致死循環(huán),在jdk1.8中改為尾插法,可以避免這種問題,但是依然避免不了節(jié)點丟失的問題。

建議

HashMap的設(shè)計初衷就不是在并發(fā)情況下使用,如果有并發(fā)的場景,推薦使用ConcurrentHashMap

關(guān)注公眾號:java之旅

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 不足的地方請大家多多指正,如有其它沒有想到的常問面試題請大家多多評論,一起成長,感謝!~ String可以被繼承嗎...
    啟示錄是真的閱讀 3,065評論 3 3
  • 本系列出于AWeiLoveAndroid的分享,在此感謝,再結(jié)合自身經(jīng)驗查漏補缺,完善答案。以成系統(tǒng)。 Java基...
    濟公大將閱讀 1,610評論 1 6
  • Java SE 基礎(chǔ): 封裝、繼承、多態(tài) 封裝: 概念:就是把對象的屬性和操作(或服務(wù))結(jié)合為一個獨立的整體,并盡...
    Jayden_Cao閱讀 2,234評論 0 8
  • 上一篇文章中提到了ThreadLocalMap是使用開放地址法來解決沖突問題的,而我們今天的主角HashMap是采...
    zy_think123閱讀 2,336評論 1 2
  • 一 秀恩愛這件事,實在是讓人又愛又恨。究竟要不要秀,應(yīng)不應(yīng)該秀,自始至終都沒有一個定論。 秀恩愛,死得快。這大概是...
    藍小巫閱讀 713評論 2 6

友情鏈接更多精彩內(nèi)容