面試必考之HashMap在并發(fā)編程環(huán)境下死循環(huán)

HashMap在并發(fā)編程環(huán)境下有什么問題啊?

代碼回顧

1.7 的時候截珍,HashMap 在多并發(fā)環(huán)境下论熙,多線程擴(kuò)容螃成,有可能引起的死循環(huán)問題:

如1.7 的時候犹菱,HashMap的底層是由數(shù)組加鏈表來實現(xiàn)的朗儒,如下所示:


jdk7-HashMap 數(shù)據(jù)結(jié)構(gòu).png

當(dāng)往HashMap中put一個鍵值對時:

public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

方法addEntry(hash, key, value, i);

void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }

當(dāng)達(dá)到擴(kuò)容條件為 if ((size >= threshold) && (null != table[bucketIndex])) 成立時颊乘,會發(fā)生 resize(2 * table.length); 擴(kuò)容。

比如 table的初始容量為8,加載因子為0.75,此時閾值為6醉锄,table已有三個元素乏悄,現(xiàn)在put一個元素 Entry8 , Entry8 被散列到table[5]處,而table[5] != null,此時滿足擴(kuò)容條件.

jdk7 HashMap 數(shù)據(jù)結(jié)構(gòu)插入擴(kuò)容.png

擴(kuò)容時先生成一個是table大小2倍的newTable榆鼠,然后把table中的元素遷移到newTable中

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

遷移的工作就由 transfer方法來完成纲爸,這個方法就是引起死循環(huán)的原因所在.

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

環(huán)的形成

現(xiàn)在我們來模擬一下環(huán)是如何形成的,設(shè)HashMap的初始容量為4妆够,先往HashMap中依次put三個元素<5,”B”>识啦、<9,”C”>、<1,”A”>神妹,它們都被散列到table[1]處颓哮,形成了鏈


模擬擴(kuò)容前.png

假設(shè)一個HashMap已經(jīng)到了Resize的臨界點。此時有兩個線程Thread1和Thread2鸵荠,在同一時刻對HashMap進(jìn)行Put操作:


模擬擴(kuò)容前插入.png

此時達(dá)到Resize條件冕茅,兩個線程各自進(jìn)行resize的第一步,也就是擴(kuò)容:


模擬同時擴(kuò)容.png

此時蛹找, 這時候姨伤,兩個線程都走到了rehash的步驟。假設(shè)線程1執(zhí)行完 transfer 方法中的 Entry<K,V> next = e.next 語句后時間片用完庸疾,掛起

線程1掛起.png

此時乍楚,線程1內(nèi)的 e 指向 <1,“A”>,next指向<9,”C”> 届慈,如下圖所示:


線程1掛起狀態(tài).png

線程2 生成完newTable后用transfer方法進(jìn)行遷移徒溪,執(zhí)行完\color{red}{Entry<K,V> next = e.next;}后與線程1一樣忿偷,e指向<1,”A”>,next指向<9,”C”>

線程2第一次循環(huán)狀態(tài).png

線程2 然后繼續(xù)執(zhí)行循環(huán)體下面的語句

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;
                // 繼續(xù)執(zhí)行
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                // 此時 e = <1,A>
                // 下面的語句會把運行時的變量的值填進(jìn)去
                //注意: <1,A> 代表了指向 <1,A>的引用,就是此e的值
                e.next = newTable[i];
                // <1,A>.next = newTable[1];
                newTable[i] = e;
                // newTable[1] = <1,A>;
                e = next;
                // <1,A> = <9,C>;
                // 此時e的指針指向了<9,C>
            }
        }

線程2執(zhí)行完之后為下圖 :


線程2執(zhí)行完第一次循環(huán)狀態(tài).png

然后線程2按照同樣的邏輯進(jìn)行第二次循環(huán)(while循環(huán))臊泌,下圖是第二遍循環(huán)后的結(jié)果 :

線程2執(zhí)行完第二次循環(huán)狀態(tài).png

然后線程2按照同樣的邏輯進(jìn)行第三遍循環(huán)鲤桥,執(zhí)行后的結(jié)果 :


線程2執(zhí)行完第三次循環(huán)狀態(tài).png

此時,線程2時間片用完渠概,線程1得到時間片茶凳,開始執(zhí)行 :


線程1繼續(xù)執(zhí)行.png

注意,此時線程1的e指向<1,A>,next指向<9,C>,在線程2操作鏈表的時候播揪,線程1 中的e,next指向的元素沒變(一直是紅色的e和next)

線程2時間片用完.png

線程一和線程二整體圖為:


線程1重新開始整體圖.png

然后線程1開始執(zhí)行循環(huán)內(nèi)剩下的代碼 :

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;
                // 繼續(xù)執(zhí)行
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                // 此時 e = <1,A>
                // 下面的語句會把運行時的變量的值填進(jìn)去
                //注意: <1,A> 代表了指向 <1,A>的引用慧妄,就是此e的值
                e.next = newTable[i];
                // <1,A>.next = newTable[1];
                newTable[i] = e;
                // newTable[1] = <1,A>;
                e = next;
                // <1,A> = <9,C>;
                // 此時e的指針指向了<9,C>
            }
        }

線程1執(zhí)行完之后為下圖 :


線程1執(zhí)行第一次循環(huán).png

線程1繼續(xù)執(zhí)行第二遍循環(huán) :

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;
                // next = <1,A>
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                // 此時 e = <9,C>
                e.next = newTable[i];
                // <9,C>.next = newTable[1];
                newTable[i] = e;
                // newTable[1] = <9,C>;
                e = next;
                // <9,C> = <1,A>;
                // 此時e的指針指向了<1,A>
            }
        }

執(zhí)行完之后如下圖所示:


線程1執(zhí)行第二次循環(huán).png

線程1繼續(xù)第三遍循環(huán)(注意:此次循環(huán)會形成環(huán)):
先執(zhí)行 \color{#FF0000}{Entry<K,V> next = <1,A>.next }

線程1執(zhí)行第3-1次循環(huán).png

然后執(zhí)行 \color{#FF0000}{<1,A>.next = newTable[1] }

線程1執(zhí)行第3-2次循環(huán).png

此時環(huán)已經(jīng)形成

然后執(zhí)行剩余的兩行代碼

newTable[i] = e;
// newTable[1] = <1,A>;
e = next;
// e = null;

執(zhí)行完,e 為 null,退出循環(huán)


線程1執(zhí)行第3-3次循環(huán).png

死循環(huán)的發(fā)生

當(dāng)形成環(huán)后,當(dāng)往HashMap中put元素的時候剪芍,這個元素恰巧落在了table[1](不管有沒有更新table),而這個元素的Key不在table[1]這條鏈上窟蓝,此時會發(fā)生死循環(huán)

put死循環(huán).png

或者在get元素的時候罪裹,該元素恰巧落在table[1]上,并且該元素的Key不在該鏈上运挫,會發(fā)生死循環(huán) :


get死循環(huán).png

死循環(huán)是這樣形成的状共, 核心就是線程2修改了原來的鏈表結(jié)構(gòu),而線程1卻以原來的邏輯執(zhí)行遷移 谁帕。

jdk1.8 會產(chǎn)生死循環(huán)么峡继?

JDK 8 用 head 和 tail 來保證鏈表的順序和之前一樣; 并且采用尾插法匈挖,避免了這種死循環(huán)的產(chǎn)生碾牌, 但是還會有數(shù)據(jù)丟失等弊端(并發(fā)本身的問題)。

那并發(fā)下我還想使用散列表這種數(shù)據(jù)結(jié)構(gòu)怎么辦呢 儡循?

多線程情況下還是建議使用 ConcurrentHashMap舶吗,鎖的粒度較小,較HashTable或者 Collections.synchronizedMap() 性能要好一些 择膝。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末誓琼,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子肴捉,更是在濱河造成了極大的恐慌腹侣,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,919評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件齿穗,死亡現(xiàn)場離奇詭異傲隶,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)缤灵,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,567評論 3 392
  • 文/潘曉璐 我一進(jìn)店門伦籍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蓝晒,“玉大人,你說我怎么就攤上這事帖鸦≈マ保” “怎么了?”我有些...
    開封第一講書人閱讀 163,316評論 0 353
  • 文/不壞的土叔 我叫張陵作儿,是天一觀的道長洛二。 經(jīng)常有香客問我,道長攻锰,這世上最難降的妖魔是什么晾嘶? 我笑而不...
    開封第一講書人閱讀 58,294評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮娶吞,結(jié)果婚禮上垒迂,老公的妹妹穿的比我還像新娘。我一直安慰自己妒蛇,他們只是感情好机断,可當(dāng)我...
    茶點故事閱讀 67,318評論 6 390
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著绣夺,像睡著了一般吏奸。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上陶耍,一...
    開封第一講書人閱讀 51,245評論 1 299
  • 那天奋蔚,我揣著相機(jī)與錄音,去河邊找鬼烈钞。 笑死泊碑,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的棵磷。 我是一名探鬼主播蛾狗,決...
    沈念sama閱讀 40,120評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼仪媒!你這毒婦竟也來了沉桌?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,964評論 0 275
  • 序言:老撾萬榮一對情侶失蹤算吩,失蹤者是張志新(化名)和其女友劉穎留凭,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體偎巢,經(jīng)...
    沈念sama閱讀 45,376評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡蔼夜,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,592評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了压昼。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片求冷。...
    茶點故事閱讀 39,764評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡瘤运,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出匠题,到底是詐尸還是另有隱情拯坟,我是刑警寧澤,帶...
    沈念sama閱讀 35,460評論 5 344
  • 正文 年R本政府宣布韭山,位于F島的核電站郁季,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏钱磅。R本人自食惡果不足惜梦裂,卻給世界環(huán)境...
    茶點故事閱讀 41,070評論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望盖淡。 院中可真熱鬧年柠,春花似錦、人聲如沸褪迟。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,697評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽牵咙。三九已至,卻和暖如春攀唯,著一層夾襖步出監(jiān)牢的瞬間洁桌,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,846評論 1 269
  • 我被黑心中介騙來泰國打工侯嘀, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留另凌,地道東北人。 一個月前我還...
    沈念sama閱讀 47,819評論 2 370
  • 正文 我出身青樓戒幔,卻偏偏與公主長得像吠谢,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子诗茎,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,665評論 2 354

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