最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Java京東面試題之為什么HashMap線程不安全

 更新時(shí)間:2021年11月04日 16:11:13   作者:沉默王二  
那天,小二去京東面試,面試官老王一上來(lái)就甩給了他一道面試題:為什么 HashMap 是線程不安全的?這個(gè)問(wèn)題哪能難的住小二,這篇文章詳細(xì)解答該題目

01、多線程下擴(kuò)容會(huì)死循環(huán)

眾所周知,HashMap 是通過(guò)拉鏈法來(lái)解決哈希沖突的,也就是當(dāng)哈希沖突時(shí),會(huì)將相同哈希值的鍵值對(duì)通過(guò)鏈表的形式存放起來(lái)。

JDK 7 時(shí),采用的是頭部插入的方式來(lái)存放鏈表的,也就是下一個(gè)沖突的鍵值對(duì)會(huì)放在上一個(gè)鍵值對(duì)的前面(同一位置上的新元素被放在鏈表的頭部)。擴(kuò)容的時(shí)候就有可能導(dǎo)致出現(xiàn)環(huán)形鏈表,造成死循環(huán)。

resize 方法的源碼:

// newCapacity為新的容量
void resize(int newCapacity) {
    // 小數(shù)組,臨時(shí)過(guò)度下
    Entry[] oldTable = table;
    // 擴(kuò)容前的容量
    int oldCapacity = oldTable.length;
    // MAXIMUM_CAPACITY 為最大容量,2 的 30 次方 = 1<<30
    if (oldCapacity == MAXIMUM_CAPACITY) {
        // 容量調(diào)整為 Integer 的最大值 0x7fffffff(十六進(jìn)制)=2 的 31 次方-1
        threshold = Integer.MAX_VALUE;
        return;
    }

    // 初始化一個(gè)新的數(shù)組(大容量)
    Entry[] newTable = new Entry[newCapacity];
    // 把小數(shù)組的元素轉(zhuǎn)移到大數(shù)組中
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    // 引用新的大數(shù)組
    table = newTable;
    // 重新計(jì)算閾值
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

transfer 方法用來(lái)轉(zhuǎn)移,將小數(shù)組的元素拷貝到新的數(shù)組中。

void transfer(Entry[] newTable, boolean rehash) {
    // 新的容量
    int newCapacity = newTable.length;
    // 遍歷小數(shù)組
    for (Entry<K,V> e : table) {
        while(null != e) {
            // 拉鏈法,相同 key 上的不同值
            Entry<K,V> next = e.next;
            // 是否需要重新計(jì)算 hash
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            // 根據(jù)大數(shù)組的容量,和鍵的 hash 計(jì)算元素在數(shù)組中的下標(biāo)
            int i = indexFor(e.hash, newCapacity);

            // 同一位置上的新元素被放在鏈表的頭部
            e.next = newTable[i];

            // 放在新的數(shù)組上
            newTable[i] = e;

            // 鏈表上的下一個(gè)元素
            e = next;
        }
    }
}

注意 e.next = newTable[i]newTable[i] = e 這兩行代碼,就會(huì)將同一位置上的新元素被放在鏈表的頭部。

擴(kuò)容前的樣子假如是下面這樣子。

那么正常擴(kuò)容后就是下面這樣子。

假設(shè)現(xiàn)在有兩個(gè)線程同時(shí)進(jìn)行擴(kuò)容,線程 A 在執(zhí)行到 newTable[i] = e; 被掛起,此時(shí)線程 A 中:e=3、next=7、e.next=null

線程 B 開始執(zhí)行,并且完成了數(shù)據(jù)轉(zhuǎn)移。

此時(shí),7 的 next 為 3,3 的 next 為 null。

隨后線程A獲得CPU時(shí)間片繼續(xù)執(zhí)行 newTable[i] = e,將3放入新數(shù)組對(duì)應(yīng)的位置,執(zhí)行完此輪循環(huán)后線程A的情況如下:

執(zhí)行下一輪循環(huán),此時(shí) e=7,原本線程 A 中 7 的 next 為 5,但由于 table 是線程 A 和線程 B 共享的,而線程 B 順利執(zhí)行完后,7 的 next 變成了 3,那么此時(shí)線程 A 中,7 的 next 也為 3 了。

采用頭部插入的方式,變成了下面這樣子:

好像也沒(méi)什么問(wèn)題,此時(shí) next = 3,e = 3。

進(jìn)行下一輪循環(huán),但此時(shí),由于線程 B 將 3 的 next 變?yōu)榱?null,所以此輪循環(huán)應(yīng)該是最后一輪了。

接下來(lái)當(dāng)執(zhí)行完 e.next=newTable[i] 即 3.next=7 后,3 和 7 之間就相互鏈接了,執(zhí)行完 newTable[i]=e 后,3 被頭插法重新插入到鏈表中,執(zhí)行結(jié)果如下圖所示:

套娃開始,元素 5 也就成了棄嬰,慘~~~

不過(guò),JDK 8 時(shí)已經(jīng)修復(fù)了這個(gè)問(wèn)題,擴(kuò)容時(shí)會(huì)保持鏈表原來(lái)的順序,參照HashMap 擴(kuò)容機(jī)制的這一篇。

02、多線程下 put 會(huì)導(dǎo)致元素丟失

正常情況下,當(dāng)發(fā)生哈希沖突時(shí),HashMap 是這樣的:

但多線程同時(shí)執(zhí)行 put 操作時(shí),如果計(jì)算出來(lái)的索引位置是相同的,那會(huì)造成前一個(gè) key 被后一個(gè) key 覆蓋,從而導(dǎo)致元素的丟失。

put 的源碼:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;

    // 步驟①:tab為空則創(chuàng)建
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;

    // 步驟②:計(jì)算index,并對(duì)null做處理 
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;

        // 步驟③:節(jié)點(diǎn)key存在,直接覆蓋value
        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);

                    //鏈表長(zhǎng)度大于8轉(zhuǎn)換為紅黑樹進(jìn)行處理
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }

                // key已經(jīng)存在直接覆蓋value
                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;

    // 步驟⑦:超過(guò)最大容量 就擴(kuò)容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

問(wèn)題發(fā)生在步驟 ② 這里:

if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);

兩個(gè)線程都執(zhí)行了 if 語(yǔ)句,假設(shè)線程 A 先執(zhí)行了 tab[i] = newNode(hash, key, value, null),那 table 是這樣的:

接著,線程 B 執(zhí)行了 tab[i] = newNode(hash, key, value, null),那 table 是這樣的:

3 被干掉了。

03、put 和 get 并發(fā)時(shí)會(huì)導(dǎo)致 get 到 null

線程 A 執(zhí)行put時(shí),因?yàn)樵貍€(gè)數(shù)超出閾值而出現(xiàn)擴(kuò)容,線程B 此時(shí)執(zhí)行g(shù)et,有可能導(dǎo)致這個(gè)問(wèn)題。

注意來(lái)看 resize 源碼:

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        // 超過(guò)最大值就不再擴(kuò)充了,就只好隨你碰撞去吧
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 沒(méi)超過(guò)最大值,就擴(kuò)充為原來(lái)的2倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 計(jì)算新的resize上限
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
}

線程 A 執(zhí)行完 table = newTab 之后,線程 B 中的 table 此時(shí)也發(fā)生了變化,此時(shí)去 get 的時(shí)候當(dāng)然會(huì) get 到 null 了,因?yàn)樵剡€沒(méi)有轉(zhuǎn)移。

這是《Java 程序員進(jìn)階之路》專欄的第 58 篇,我們來(lái)聊了聊為什么 HashMap 是線程不安全的。

為了便于大家更系統(tǒng)化地學(xué)習(xí) Java,二哥已經(jīng)將《Java 程序員進(jìn)階之路》專欄開源到 GitHub 上了,大家只需輕輕地 star 一下,就可以和所有的小伙伴一起打怪升級(jí)了。

GitHub 地址:https://github.com/itwanger/toBeBetterJavaer

到此這篇關(guān)于Java京東面試題之為什么HashMap線程不安全的文章就介紹到這了,更多相關(guān)Java HashMap線程內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • springIoc及注解的使用實(shí)例詳解

    springIoc及注解的使用實(shí)例詳解

    注解(Annotation)是一種在 Java 程序中以元數(shù)據(jù)的形式對(duì)代碼進(jìn)行標(biāo)記和說(shuō)明的機(jī)制,它可以被添加到類、方法、字段、參數(shù)等程序元素上,用于提供額外的信息和指示,本文給大家介紹springIoc及注解的使用,感興趣的朋友一起看看吧
    2024-02-02
  • SpringCache常用注解及key中參數(shù)值為null問(wèn)題解析

    SpringCache常用注解及key中參數(shù)值為null問(wèn)題解析

    這篇文章主要介紹了SpringCache常用注解及key中參數(shù)值為null的問(wèn)題解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-09-09
  • Java經(jīng)典排序算法之歸并排序詳解

    Java經(jīng)典排序算法之歸并排序詳解

    這篇文章主要為大家詳細(xì)介紹了Java經(jīng)典排序算法之歸并排序,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2017-04-04
  • SpringBoot 配置提示功能(超詳細(xì))

    SpringBoot 配置提示功能(超詳細(xì))

    這篇文章主要介紹了SpringBoot 配置提示功能,本文給大家介紹的超詳細(xì),通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2019-10-10
  • SpringBoot對(duì)接AWS?S3實(shí)現(xiàn)上傳和查詢

    SpringBoot對(duì)接AWS?S3實(shí)現(xiàn)上傳和查詢

    AWS?S3是亞馬遜提供的一種對(duì)象存儲(chǔ)服務(wù),旨在提供可擴(kuò)展、高可用性和安全的數(shù)據(jù)存儲(chǔ)解決方案,本文我們就來(lái)看看SpringBoot如何對(duì)接AWS?S3實(shí)現(xiàn)上傳和查詢吧
    2025-02-02
  • vue數(shù)據(jù)響應(yīng)式原理重寫函數(shù)實(shí)現(xiàn)數(shù)組響應(yīng)式監(jiān)聽

    vue數(shù)據(jù)響應(yīng)式原理重寫函數(shù)實(shí)現(xiàn)數(shù)組響應(yīng)式監(jiān)聽

    Vue的通過(guò)數(shù)據(jù)劫持的方式實(shí)現(xiàn)數(shù)據(jù)的雙向綁定,即使用Object.defineProperty()來(lái)實(shí)現(xiàn)對(duì)屬性的劫持,但是Object.defineProperty()中的setter是無(wú)法直接實(shí)現(xiàn)數(shù)組中值的改變的劫持行為的,需要的朋友可以參考下
    2023-05-05
  • Springboot實(shí)現(xiàn)多文件上傳代碼解析

    Springboot實(shí)現(xiàn)多文件上傳代碼解析

    這篇文章主要介紹了Springboot實(shí)現(xiàn)多文件上傳代碼解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2020-04-04
  • Spring MVC數(shù)據(jù)綁定概述及原理詳解

    Spring MVC數(shù)據(jù)綁定概述及原理詳解

    這篇文章主要介紹了Spring MVC數(shù)據(jù)綁定概述及原理詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2020-06-06
  • SpringBoot的跨域注解@CrossOrigin解析

    SpringBoot的跨域注解@CrossOrigin解析

    這篇文章主要介紹了SpringBoot的跨域注解@CrossOrigin解析,Spring Framework 4.2 GA為CORS提供了第一類支持,使您比通常的基于過(guò)濾器的解決方案更容易和更強(qiáng)大地配置它,所以springMVC的版本要在4.2或以上版本才支持@CrossOrigin,需要的朋友可以參考下
    2023-12-12
  • 詳解JDK 5 Annotation 注解之@Target的用法介紹

    詳解JDK 5 Annotation 注解之@Target的用法介紹

    這篇文章主要介紹了詳解JDK 5 Annotation 注解之@Target的用法介紹,需要的朋友可以參考下
    2016-02-02

最新評(píng)論

读书| 奉新县| 常熟市| 阿合奇县| 黔东| 海南省| 绥棱县| 来安县| 自治县| 三门县| 新源县| 建宁县| 南昌市| 商丘市| 合水县| 连城县| 延吉市| 天峨县| 皮山县| 凌海市| 永年县| 淳化县| 仁布县| 广水市| 甘孜县| 鹤壁市| 铅山县| 武城县| 芦溪县| 墨玉县| 德州市| 丰宁| 手游| 卫辉市| 布尔津县| 石棉县| 遂昌县| 景洪市| 道孚县| 朝阳县| 纳雍县|