JDK1.7和JDK1.8中HashMap為什么是線程不安全的

只要是對于集合有一定了解的一定都知道HashMap是線程不安全的携栋,我們應(yīng)該使用ConcurrentHashMap。但是為什么HashMap是線程不安全的呢咳秉,之前面試的時候也遇到到這樣的問題婉支,但是當(dāng)時只停留在知道是的層面上,并沒有深入理解為什么是澜建。于是今天重溫一個HashMap線程不安全的這個問題磅摹。

首先需要強調(diào)一點滋迈,HashMap的線程不安全體現(xiàn)在會造成死循環(huán)、數(shù)據(jù)丟失户誓、數(shù)據(jù)覆蓋這些問題饼灿。其中死循環(huán)和數(shù)據(jù)丟失是在JDK1.7中出現(xiàn)的問題,在JDK1.8中已經(jīng)得到解決帝美,然而1.8中仍會有數(shù)據(jù)覆蓋這樣的問題碍彭。

擴容引發(fā)的線程不安全

HashMap的線程不安全主要是發(fā)生在擴容函數(shù)中,即根源是在transfer函數(shù)中悼潭,JDK1.7中HashMaptransfer函數(shù)如下:

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;
            }
        }
    }

這段代碼是HashMap的擴容操作庇忌,重新定位每個桶的下標(biāo),并采用頭插法將元素遷移到新數(shù)組中舰褪。頭插法會將鏈表的順序翻轉(zhuǎn)皆疹,這也是形成死循環(huán)的關(guān)鍵點。理解了頭插法后再繼續(xù)往下看是如何造成死循環(huán)以及數(shù)據(jù)丟失的占拍。

擴容造成死循環(huán)和數(shù)據(jù)丟失的分析過程

假設(shè)現(xiàn)在有兩個線程A略就、B同時對下面這個HashMap進(jìn)行擴容操作:

image

正常擴容后的結(jié)果是下面這樣的:


image

但是當(dāng)線程A執(zhí)行到上面transfer函數(shù)的第11行代碼時,CPU時間片耗盡晃酒,線程A被掛起表牢。即如下圖中位置所示:

image

此時線程A中:e=3、next=7贝次、e.next=null


image

當(dāng)線程A的時間片耗盡后崔兴,CPU開始執(zhí)行線程B,并在線程B中成功的完成了數(shù)據(jù)遷移


image

重點來了蛔翅,根據(jù)Java內(nèi)存模式可知敲茄,線程B執(zhí)行完數(shù)據(jù)遷移后,此時主內(nèi)存中newTabletable都是最新的山析,也就是說:7.next=3折汞、3.next=null。

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

image

接著繼續(xù)執(zhí)行下一輪循環(huán)损同,此時e=7翩腐,從主內(nèi)存中讀取e.next時發(fā)現(xiàn)主內(nèi)存中7.next=3,于是乎next=3膏燃,并將7采用頭插法的方式放入新數(shù)組中茂卦,并繼續(xù)執(zhí)行完此輪循環(huán),結(jié)果如下:


image

執(zhí)行下一次循環(huán)可以發(fā)現(xiàn)组哩,next=e.next=null等龙,所以此輪循環(huán)將會是最后一輪循環(huán)处渣。接下來當(dāng)執(zhí)行完e.next=newTable[i]即3.next=7后,3和7之間就相互連接了蛛砰,當(dāng)執(zhí)行完newTable[i]=e后罐栈,3被頭插法重新插入到鏈表中,執(zhí)行結(jié)果如下圖所示:


image

上面說了此時e.next=null即next=null泥畅,當(dāng)執(zhí)行完e=null后荠诬,將不會進(jìn)行下一輪循環(huán)。到此線程A位仁、B的擴容操作完成柑贞,很明顯當(dāng)線程A執(zhí)行完后,HashMap中出現(xiàn)了環(huán)形結(jié)構(gòu)聂抢,當(dāng)在以后對該HashMap進(jìn)行操作時會出現(xiàn)死循環(huán)钧嘶。

并且從上圖可以發(fā)現(xiàn),元素5在擴容期間被莫名的丟失了琳疏,這就發(fā)生了數(shù)據(jù)丟失的問題有决。

JDK1.8中的線程不安全

根據(jù)上面JDK1.7出現(xiàn)的問題,在JDK1.8中已經(jīng)得到了很好的解決轿亮,如果你去閱讀1.8的源碼會發(fā)現(xiàn)找不到transfer函數(shù)疮薇,因為JDK1.8直接在resize函數(shù)中完成了數(shù)據(jù)遷移。另外說一句我注,JDK1.8在進(jìn)行元素插入時使用的是尾插法按咒。

為什么說JDK1.8會出現(xiàn)數(shù)據(jù)覆蓋的情況喃,我們來看一下下面這段JDK1.8中的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;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null) // 如果沒有hash碰撞則直接插入元素
            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;
    }

其中第六行代碼是判斷是否出現(xiàn)hash碰撞但骨,假設(shè)兩個線程A励七、B都在進(jìn)行put操作,并且hash函數(shù)計算出的插入下標(biāo)是相同的奔缠,當(dāng)線程A執(zhí)行完第六行代碼后由于時間片耗盡導(dǎo)致被掛起掠抬,而線程B得到時間片后在該下標(biāo)處插入了元素,完成了正常的插入校哎,然后線程A獲得時間片两波,由于之前已經(jīng)進(jìn)行了hash碰撞的判斷,所有此時不會再進(jìn)行判斷闷哆,而是直接進(jìn)行插入腰奋,這就導(dǎo)致了線程B插入的數(shù)據(jù)被線程A覆蓋了,從而線程不安全抱怔。

除此之前劣坊,還有就是代碼的第38行處有個++size,我們這樣想屈留,還是線程A局冰、B测蘑,這兩個線程同時進(jìn)行put操作時,假設(shè)當(dāng)前HashMap的zise大小為10康二,當(dāng)線程A執(zhí)行到第38行代碼時碳胳,從主內(nèi)存中獲得size的值為10后準(zhǔn)備進(jìn)行+1操作,但是由于時間片耗盡只好讓出CPU赠摇,線程B快樂的拿到CPU還是從主內(nèi)存中拿到size的值10進(jìn)行+1操作固逗,完成了put操作并將size=11寫回主內(nèi)存,然后線程A再次拿到CPU并繼續(xù)執(zhí)行(此時size的值仍為10)藕帜,當(dāng)執(zhí)行完put操作后烫罩,還是將size=11寫回內(nèi)存,此時洽故,線程A贝攒、B都執(zhí)行了一次put操作,但是size的值只增加了1时甚,所有說還是由于數(shù)據(jù)覆蓋又導(dǎo)致了線程不安全隘弊。

總結(jié)

HashMap的線程不安全主要體現(xiàn)在下面兩個方面:
1.在JDK1.7中,當(dāng)并發(fā)執(zhí)行擴容操作時會造成環(huán)形鏈和數(shù)據(jù)丟失的情況荒适。
2.在JDK1.8中梨熙,在并發(fā)執(zhí)行put操作時會發(fā)生數(shù)據(jù)覆蓋的情況。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末刀诬,一起剝皮案震驚了整個濱河市咽扇,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌陕壹,老刑警劉巖质欲,帶你破解...
    沈念sama閱讀 216,651評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異糠馆,居然都是意外死亡嘶伟,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評論 3 392
  • 文/潘曉璐 我一進(jìn)店門又碌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來九昧,“玉大人,你說我怎么就攤上這事毕匀≈ィ” “怎么了?”我有些...
    開封第一講書人閱讀 162,931評論 0 353
  • 文/不壞的土叔 我叫張陵期揪,是天一觀的道長。 經(jīng)常有香客問我规个,道長凤薛,這世上最難降的妖魔是什么姓建? 我笑而不...
    開封第一講書人閱讀 58,218評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮缤苫,結(jié)果婚禮上速兔,老公的妹妹穿的比我還像新娘。我一直安慰自己活玲,他們只是感情好涣狗,可當(dāng)我...
    茶點故事閱讀 67,234評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著舒憾,像睡著了一般镀钓。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上镀迂,一...
    開封第一講書人閱讀 51,198評論 1 299
  • 那天丁溅,我揣著相機與錄音,去河邊找鬼探遵。 笑死窟赏,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的箱季。 我是一名探鬼主播涯穷,決...
    沈念sama閱讀 40,084評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼藏雏!你這毒婦竟也來了拷况?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,926評論 0 274
  • 序言:老撾萬榮一對情侶失蹤诉稍,失蹤者是張志新(化名)和其女友劉穎蝠嘉,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體杯巨,經(jīng)...
    沈念sama閱讀 45,341評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡蚤告,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,563評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了服爷。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片杜恰。...
    茶點故事閱讀 39,731評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖仍源,靈堂內(nèi)的尸體忽然破棺而出心褐,到底是詐尸還是另有隱情,我是刑警寧澤笼踩,帶...
    沈念sama閱讀 35,430評論 5 343
  • 正文 年R本政府宣布逗爹,位于F島的核電站,受9級特大地震影響嚎于,放射性物質(zhì)發(fā)生泄漏掘而。R本人自食惡果不足惜挟冠,卻給世界環(huán)境...
    茶點故事閱讀 41,036評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望袍睡。 院中可真熱鬧知染,春花似錦、人聲如沸斑胜。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽止潘。三九已至掺炭,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間覆山,已是汗流浹背竹伸。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留簇宽,地道東北人勋篓。 一個月前我還...
    沈念sama閱讀 47,743評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像魏割,于是被迫代替她去往敵國和親譬嚣。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,629評論 2 354

推薦閱讀更多精彩內(nèi)容