為什么HashMap是線程不安全的?

一、背景

HashMap的擴容機制就是重新申請一個容量是當前容量2倍的桶數組,然后將舊數組中的元素逐個映射到新的數組里邊,然后將原先的桶逐個置為引用失效。HashMap之所以線程不安全,就是resize這里出的問題。

二、線程不安全場景

1、put的時候導致的多線程數據不一致。
這個問題比較好想象,比如有兩個線程A和B,首先A希望先插入一個key-value到HashMap中,首先計算key-value索引位置,然后獲取到該桶里面鏈表的頭節(jié)點,并且key不存在桶中,需要新建節(jié)點插入,此時線程A的時間片用完了;線程B被調度得以執(zhí)行,和線程A一樣執(zhí)行,但線程B比較幸運,成功的將key-vaule插入到了鏈表中,隨后線程A再次被調度運行時,它依然持有過期的鏈表頭但它對此一無所知,如此一來就覆蓋了線程B插入的記錄,但線程B插入的記錄就憑空消失了。

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

    // 1. 把table中該位置原來的Entry保存  
    Entry<K,V> e = table[bucketIndex];

    // 2. 在table中該位置新建一個Entry:將原頭結點位置(數組上)的鍵值對 放入到(鏈表)后1個節(jié)點中、將需插入的鍵值對 放入到頭結點中(數組上)-> 從而形成鏈表
    // 即 在插入元素時,是在鏈表頭插入的,table中的每個位置永遠只保存最新插入的Entry,舊的Entry則放入到鏈表中(即 解決Hash沖突)
    table[bucketIndex] = new Entry<>(hash, key, value, e);  

    // 3. 哈希表的鍵值對數量計數增加
    size++;  
}

2、HashMap的get操作死循環(huán)
達到resize條件,兩個線程各自進行resize的第一步-擴容。讓我們回顧一下ReHash源碼

//resieze的核心源碼如下
//這個方法的功能時將原來的記錄重新計算在新桶的位置,然后遷移過去
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()過程中,在將舊數組上的數據 轉移到 新數組上時,轉移數據操作 = 按舊鏈表的正序遍歷鏈表、在新鏈表的頭部依次插入,即在轉移數據、擴容后,容易出現(xiàn)鏈表逆序的情況
此時若(多線程)并發(fā)執(zhí)行 put()操作,一旦出現(xiàn)擴容情況,則 容易出現(xiàn) 環(huán)形鏈表,從而在獲取數據、遍歷鏈表時 形成死循環(huán)(Infinite Loop),即 死鎖的狀態(tài)。


圖片1.png

假如此時線程遍歷到Entry3對象,剛執(zhí)行完紅圈里的這行代碼,線程就被掛起
圖片13.png

對于線程B來說e=Entry3,next=Entry2;
這時候線程A暢通無阻的進行著Rehash,當ReHash完成后,結果如下


圖片2.png

接下來線程B恢復,繼續(xù)執(zhí)行屬于它自己的ReHash,線程B剛才的狀態(tài)是e = Entry3,next=Entry2
當執(zhí)行紅框內的一行代碼時


圖片8.png

顯然i = 3,因為剛才線程A對于Entry3的hash結果也是3.
我們繼續(xù)執(zhí)行


圖片9.png

Entry3放入線程B的數組下標為3的位置,并且e指向了Entry2。

接著新一輪的循環(huán),又執(zhí)行到


圖片10.png

e = Entry2
next = Entry3


圖片3.png

接下來執(zhí)行


圖片11.png

用頭插法把Entry2插入到線程B的數組的頭節(jié)點


圖片4.png

接下來執(zhí)行紅框內的代碼


圖片5.png

e = Entry3
next = null
當我們執(zhí)行下面這一行的時候,見證奇跡的時刻來臨了:


圖片6.png

newTable[i] = Entry2
e = Entry3
Entry2.next = Entry3
Entry3.next = Entry2
鏈表出現(xiàn)了環(huán)形!


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

相關閱讀更多精彩內容

友情鏈接更多精彩內容