日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) >

Java HashMap的死循环问题

發(fā)布時(shí)間:2025/3/18 45 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Java HashMap的死循环问题 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

看到過(guò)很多CPU被100%的線上故障,并且這個(gè)事發(fā)生了很多次,原因是在Java語(yǔ)言在并發(fā)情況下使用HashMap造成Race Condition,從而導(dǎo)致死循環(huán)。這個(gè)事情我4、5年前也經(jīng)歷過(guò),本來(lái)覺(jué)得沒(méi)什么好寫的,因?yàn)镴ava的HashMap是非線程安全的,所以在并發(fā)下必然出現(xiàn)問(wèn)題。但是,我發(fā)現(xiàn)近幾年,很多人都經(jīng)歷過(guò)這個(gè)事(在網(wǎng)上查“HashMap Infinite Loop”可以看到很多人都在說(shuō)這個(gè)事)所以,覺(jué)得這個(gè)是個(gè)普遍問(wèn)題,需要寫篇疫苗文章說(shuō)一下這個(gè)事,并且給大家看看一個(gè)完美的“Race Condition”是怎么形成的。

問(wèn)題的癥狀

從前我們的Java代碼因?yàn)橐恍┰蚴褂昧薍ashMap這個(gè)東西,但是當(dāng)時(shí)的程序是單線程的,一切都沒(méi)有問(wèn)題。后來(lái),我們的程序性能有問(wèn)題,所以需要變成多線程的,于是,變成多線程后到了線上,發(fā)現(xiàn)程序經(jīng)常占了100%的CPU,查看堆棧,你會(huì)發(fā)現(xiàn)程序都Hang在了HashMap.get()這個(gè)方法上了,重啟程序后問(wèn)題消失。但是過(guò)段時(shí)間又會(huì)來(lái)。而且,這個(gè)問(wèn)題在測(cè)試環(huán)境里可能很難重現(xiàn)。

我們簡(jiǎn)單的看一下我們自己的代碼,我們就知道HashMap被多個(gè)線程操作。而Java的文檔說(shuō)HashMap是非線程安全的,應(yīng)該用ConcurrentHashMap。

但是在這里我們可以來(lái)研究一下原因。

?

Hash表數(shù)據(jù)結(jié)構(gòu)

我需要簡(jiǎn)單地說(shuō)一下HashMap這個(gè)經(jīng)典的數(shù)據(jù)結(jié)構(gòu)。

HashMap通常會(huì)用一個(gè)指針數(shù)組(假設(shè)為table[])來(lái)做分散所有的key,當(dāng)一個(gè)key被加入時(shí),會(huì)通過(guò)Hash算法通過(guò)key算出這個(gè)數(shù)組的下標(biāo)i,然后就把這個(gè)<key, value>插到table[i]中,如果有兩個(gè)不同的key被算在了同一個(gè)i,那么就叫沖突,又叫碰撞,這樣會(huì)在table[i]上形成一個(gè)鏈表。

我們知道,如果table[]的尺寸很小,比如只有2個(gè),如果要放進(jìn)10個(gè)keys的話,那么碰撞非常頻繁,于是一個(gè)O(1)的查找算法,就變成了鏈表遍歷,性能變成了O(n),這是Hash表的缺陷(可參看《Hash Collision DoS 問(wèn)題》)。

所以,Hash表的尺寸和容量非常的重要。一般來(lái)說(shuō),Hash表這個(gè)容器當(dāng)有數(shù)據(jù)要插入時(shí),都會(huì)檢查容量有沒(méi)有超過(guò)設(shè)定的thredhold,如果超過(guò),需要增大Hash表的尺寸,但是這樣一來(lái),整個(gè)Hash表里的無(wú)素都需要被重算一遍。這叫rehash,這個(gè)成本相當(dāng)?shù)拇蟆?/p>

相信大家對(duì)這個(gè)基礎(chǔ)知識(shí)已經(jīng)很熟悉了。

HashMap的rehash源代碼

下面,我們來(lái)看一下Java的HashMap的源代碼。

Put一個(gè)Key,Value對(duì)到Hash表中:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public?V put(K key, V value) { ????...... ????//算Hash值 ????int?hash = hash(key.hashCode()); ????int?i = indexFor(hash, table.length); ????//如果該key已被插入,則替換掉舊的value (鏈接操作) ????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++; ????//該key不存在,需要增加一個(gè)結(jié)點(diǎn) ????addEntry(hash, key, value, i); ????return?null; }

檢查容量是否超標(biāo)

1 2 3 4 5 6 7 8 void?addEntry(int?hash, K key, V value, int?bucketIndex) { ????Entry<K,V> e = table[bucketIndex]; ????table[bucketIndex] = new?Entry<K,V>(hash, key, value, e); ????//查看當(dāng)前的size是否超過(guò)了我們?cè)O(shè)定的閾值threshold,如果超過(guò),需要resize ????if?(size++ >= threshold) ????????resize(2?* table.length); }

新建一個(gè)更大尺寸的hash表,然后把數(shù)據(jù)從老的Hash表中遷移到新的Hash表中。

1 2 3 4 5 6 7 8 9 10 11 12 void?resize(int?newCapacity) { ????Entry[] oldTable = table; ????int?oldCapacity = oldTable.length; ????...... ????//創(chuàng)建一個(gè)新的Hash Table ????Entry[] newTable = new?Entry[newCapacity]; ????//將Old Hash Table上的數(shù)據(jù)遷移到New Hash Table上 ????transfer(newTable); ????table = newTable; ????threshold = (int)(newCapacity * loadFactor); }

遷移的源代碼,注意高亮處:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void?transfer(Entry[] newTable) { ????Entry[] src = table; ????int?newCapacity = newTable.length; ????//下面這段代碼的意思是: ????//? 從OldTable里摘一個(gè)元素出來(lái),然后放到NewTable中 ????for?(int?j = 0; j < src.length; j++) { ????????Entry<K,V> e = src[j]; ????????if?(e != null) { ????????????src[j] = null; ????????????do?{ ????????????????Entry<K,V> next = e.next; ????????????????int?i = indexFor(e.hash, newCapacity); ????????????????e.next = newTable[i]; ????????????????newTable[i] = e; ????????????????e = next; ????????????} while?(e != null); ????????} ????} }

好了,這個(gè)代碼算是比較正常的。而且沒(méi)有什么問(wèn)題。

正常的ReHash的過(guò)程

畫了個(gè)圖做了個(gè)演示。

  • 我假設(shè)了我們的hash算法就是簡(jiǎn)單的用key mod 一下表的大小(也就是數(shù)組的長(zhǎng)度)。

  • 最上面的是old hash 表,其中的Hash表的size=2, 所以key = 3, 7, 5,在mod 2以后都沖突在table[1]這里了。

  • 接下來(lái)的三個(gè)步驟是Hash表 resize成4,然后所有的<key,value> 重新rehash的過(guò)程

并發(fā)下的Rehash

1)假設(shè)我們有兩個(gè)線程。我用紅色和淺藍(lán)色標(biāo)注了一下。

我們?cè)倩仡^看一下我們的 transfer代碼中的這個(gè)細(xì)節(jié):

1 2 3 4 5 6 7 do?{ ????Entry<K,V> next = e.next; // <--假設(shè)線程一執(zhí)行到這里就被調(diào)度掛起了 ????int?i = indexFor(e.hash, newCapacity); ????e.next = newTable[i]; ????newTable[i] = e; ????e = next; } while?(e != null);

而我們的線程二執(zhí)行完成了。于是我們有下面的這個(gè)樣子。

注意,因?yàn)門hread1的 e 指向了key(3),而next指向了key(7),其在線程二rehash后,指向了線程二重組后的鏈表。我們可以看到鏈表的順序被反轉(zhuǎn)后。

2)線程一被調(diào)度回來(lái)執(zhí)行。

  • 先是執(zhí)行 newTalbe[i] = e;

  • 然后是e = next,導(dǎo)致了e指向了key(7),

  • 而下一次循環(huán)的next = e.next導(dǎo)致了next指向了key(3)

3)一切安好。

線程一接著工作。把key(7)摘下來(lái),放到newTable[i]的第一個(gè),然后把e和next往下移

4)環(huán)形鏈接出現(xiàn)。

e.next = newTable[i] 導(dǎo)致? key(3).next 指向了 key(7)

注意:此時(shí)的key(7).next 已經(jīng)指向了key(3), 環(huán)形鏈表就這樣出現(xiàn)了。

于是,當(dāng)我們的線程一調(diào)用到,HashTable.get(11)時(shí),悲劇就出現(xiàn)了——Infinite Loop。

其它

有人把這個(gè)問(wèn)題報(bào)給了Sun,不過(guò)Sun不認(rèn)為這個(gè)是一個(gè)問(wèn)題。因?yàn)镠ashMap本來(lái)就不支持并發(fā)。要并發(fā)就用ConcurrentHashmap


特別說(shuō)明:尊重作者的勞動(dòng)成果,轉(zhuǎn)載請(qǐng)注明出處哦~~~http://blog.yemou.net/article/query/info/tytfjhfascvhzxcytp94 與50位技術(shù)專家面對(duì)面20年技術(shù)見證,附贈(zèng)技術(shù)全景圖

總結(jié)

以上是生活随笔為你收集整理的Java HashMap的死循环问题的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。