?面試官問我HashMap哪里不安全?,我支支吾吾的說了這些...

本文作者: Code皮皮蝦,CSDN、掘金等各大平臺同名,有興趣的小伙伴可以點一波關注??,感謝您的支持!
?持續(xù)分享大廠面試題?
公眾號:JavaCodes



前言

HashMap在JDK7和JDK8是有了一些不同的,具體體現(xiàn)如下:

  1. JDK7HashMap底層是數(shù)組+鏈表,而JDK8是數(shù)組+鏈表+紅黑樹
  2. JDK7擴容采用頭插法,而JDK8采用尾插法
  3. JDK7的rehash是全部rehash,而JDK8是部分rehash。
  4. JDK8對于key的hash值計算相比于JDK7來說有所優(yōu)化。


如果還有興趣的小伙伴可以學習學習我的以下文章,寫的十分詳細!!

高頻考題:手寫HashMap

JDK7、8擴容源碼級詳解

JDK7、8HashMap的get()、put()流程詳解



JDK7 HashMap

JDK7HashMap在多線程環(huán)境下會出現(xiàn)死循環(huán)問題。

假如此時A、B線程同時對一個HashMap進行put操作,且HashMap剛號達到擴容條件需要進行擴容

那么這兩個線程都會取對HahsMap進行擴容(JDK7HashMap擴容調(diào)用 resize()方法,而resize()方法中需要調(diào)用transfer()方法將舊數(shù)組元素全部rehash到新數(shù)組中去==重點:這里在多線程環(huán)境下就會出現(xiàn)問題==)

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;
    //對數(shù)組的每一條鏈表遍歷rehash
    for (Entry<K,V> e : table) {
        while(null != e) {
            //保留下一個節(jié)點
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            //得到對應在新數(shù)組中的索引位置
            int i = indexFor(e.hash, newCapacity);
            
            //尾插法
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}

我們假設現(xiàn)在有一個鏈表 C——>D,且C、D擴容后計算的索引位置依然不變,那他么還在同一鏈表中

現(xiàn)在A線程進入到transfer方法拿到C和它的下一個節(jié)點D(Entry<K,V> next = e.next;)后,A線程被掛起,此時B線程正常走流程將C、D rehash到新的數(shù)組中,那么根據(jù)頭插法在新的數(shù)組中是D——>C

B執(zhí)行完之后,A線程繼續(xù)去執(zhí)行

因為A獲取到了 e = C,next = D,所以C可以進行rehash,C進行完后拿到D,發(fā)現(xiàn)D.next = C,所以D也可以進行rehash,那么此時因為D——>C,此時會再拿到C,發(fā)現(xiàn)C.next = null,但C不是null,所以C再進行rehash,此時鏈表尾 C——> D ——>C,因為此時e = NULL,所以退出循環(huán),此時出現(xiàn)死循環(huán)。C——>D——>C。


==各位可以好好想想這些話或者自己在草稿紙上畫一畫再來看下面的圖!==


圖示演示:

在這里插入圖片描述

==B正常執(zhí)行完成==

在這里插入圖片描述

==A繼續(xù)執(zhí)行==

因為A獲取到了 e = C,next = D,所以C可以進行rehash

在這里插入圖片描述


C進行完后拿到e = D,發(fā)現(xiàn)D.next = C,所以D也可以進行rehash

在這里插入圖片描述


那么此時因為D——>C,此時會再拿到C,發(fā)現(xiàn)C.next = null,但C不是null,所以C再進行rehash

在這里插入圖片描述

此時e = NULL,所以退出循環(huán),此時出現(xiàn)死循環(huán)。C——>D——>C。



JDK8 HashMap

JDK1.8會出現(xiàn)數(shù)據(jù)覆蓋的情況

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}
  • ==第6行代碼==:假設兩個線程A、B都在進行put操作,并且根據(jù)key計算出的hash值相同,那么得到得索引下標也相同,當線程A執(zhí)行完第六行代碼后由于時間片耗盡導致被掛起,而線程B得到時間片后在該下標處插入了元素,完成了正常的插入,然后線程A獲得時間片,由于之前已經(jīng)進行了hash碰撞的判斷,所有此時不會再進行判斷,而是直接進行插入,這就導致了線程B插入的數(shù)據(jù)被線程A覆蓋了,從而線程不安全。
  • ==第38行代碼==++size不安全,還是線程A、B,這兩個線程同時進行put操作時,假設當前HashMap的zise大小為10,當線程A執(zhí)行到第38行代碼時,從主內(nèi)存中獲得size的值為10后準備進行+1操作,但是由于時間片耗盡只好讓出CPU,線程B快樂的拿到CPU還是從主內(nèi)存中拿到size的值10進行+1操作,完成了put操作并將size=11寫回主內(nèi)存,然后線程A再次拿到CPU并繼續(xù)執(zhí)行(此時size的值仍為10),當執(zhí)行完put操作后,還是將size=11寫回內(nèi)存,此時,線程A、B都執(zhí)行了一次put操作,但是size的值只增加了1,所有說還是由于數(shù)據(jù)覆蓋又導致了線程不安全。





最后

我是 Code皮皮蝦,一個熱愛分享知識的 皮皮蝦愛好者,未來的日子里會不斷更新出對大家有益的博文,期待大家的關注?。?!

創(chuàng)作不易,如果這篇博文對各位有幫助,希望各位小伙伴可以一鍵三連哦!,感謝支持,我們下次再見~~~

==分享大綱==

大廠面試題 - 專題 - 簡書 (jianshu.com)

Java從入門到入墳學習路線目錄索引
開源爬蟲實例教程目錄索引


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

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

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