高并发下的 HashMap 为什么会死循环
作者 |?tech-bus.七十一
來源 |?程序員巴士
前言??HashMap并發情況下產生的死循環問題在JDK 1.7及之前版本是存在的,JDK 1.8 通過增加loHead頭節點和loTail尾節點進行了修復,雖然進行了修復,但是如果涉及到并發情況下需要使用hash表,建議使用CurrentHashMap替代HashMap來確保不會出現線程安全問題。
??JDK 1.7及之前 HashMap在并發情況下產生的循環問題,將可能致使服務器的CPU飆升至100%,那究竟是如何造成的呢,為了解答這個疑惑,今天就帶大家來了解一下線程不安全的HashMap在高并發的情況下是如何造成死循環的,要探究HashMap死循環的原因首先要從HashMap的源碼開始進行分析,這樣才能從根本上對HashMap進行理解,從而了解問題產生的原因。在分析之前我們要知道在JDK 1.7版本及之前HashMap采用的是數組 + 鏈表的數據結構,而在JDK 1.8則是采用數組 + 鏈表 + 紅黑樹的數據結構以進一步降低hash沖突后帶來的查詢損耗。
正文
一切還得從元素插入說起,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);//對key的hashcode進一步計算,確保散列均勻int?i?=?indexFor(hash,?table.length);//獲取在table中的實際位置for?(Entry<K,V>?e?=?table[i];?e?!=?null;?e?=?e.next)?{...}modCount++;//保證并發訪問時,若HashMap內部結構發生變化,快速響應失敗//重點關注這個addEntry增加元素的方法addEntry(hash,?key,?value,?i);return?null;}緊接著我們來看這個addEntry()方法,里面調用的resize()擴容方法是今天的主角
void?addEntry(int?hash,?K?key,?V?value,?int?bucketIndex)?{if?((size?>=?threshold)?&&?(null?!=?table[bucketIndex]))?{resize(2?*?table.length);//當size超過臨界閾值threshold,并且即將發生哈希沖突時進行擴容,擴容后新容量為舊容量的2倍hash?=?(null?!=?key)???hash(key)?:?0;bucketIndex?=?indexFor(hash,?table.length);//擴容后重新計算插入的位置下標}//把元素放入HashMap的桶的對應位置createEntry(hash,?key,?value,?bucketIndex);}下面我們進入到resize()法中,再揭開里面的transfer()方法的面紗,這個方法也是造成死循環的罪魁禍首
//按新的容量擴容Hash表??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;//修改HashMap的底層數組??threshold?=?(int)Math.min(newCapacity?*?loadFactor,?MAXIMUM_CAPACITY?+?1);//修改閥值??}最后一起來仔細分析這個transfer()方法
//將老的表中的數據拷貝到新的結構中??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)?{//如果是重新Hash,則需要重新計算hash值??e.hash?=?null?==?e.key???0?:?hash(e.key);??}??int?i?=?indexFor(e.hash,?newCapacity);//定位Hash桶??e.next?=?newTable[i];//元素連接到桶中,這里相當于單鏈表的插入,總是插入在最前面newTable[i]?=?e;//newTable[i]的值總是最新插入的值e?=?next;//繼續下一個元素??}??}??}添加元素達到閥值后對HashMap進行擴容,走reaize()方法,在對HashMap進行擴容時,又會調用一個transfer()對舊的HashMap中的元素進行轉移,那么我們今天要探究的死循環問題 就是發生在這個方法里的,在進行元素轉移時transfer()方法里會調用下面四行代碼
Entry<K,V>?next?=?e.next;? e.next?=?newTable[i]; newTable[i]?=?e; e?=?next;把元素插入新的HashMap中,粗略的看下這四行代碼似乎并沒有什么問題,元素進行轉移的圖如下(線程不沖突的情況下)
那么我們讓線程A、B同時訪問我這段代碼,當現A線程執行到以下代碼時
Entry<k,v>?next?=?e.next;線程A交出時間片,線程B這時候接手轉移并且完成了元素的轉移,這個時候線程A又拿到時間片并接著執行后續的代碼
執行后代碼如圖,當e = a時,這時候這時候再執行
e.next?=?newTable[i];//?a元素指向了b元素,產生了循環??在鏈表產生了循環后,當get()方法獲取元素的時候正好落在這個循環的鏈表上時,線程會一直在環里遍歷,無法跳出,從而導致CPU飆升至100%!
總結
在多線程情況下盡量不要用HashMap,可以用線程安全的hash表來代替,例如使用下面這些
oncurrentHashMap、HashTable、Collections.synchronizedMap() 來避免產生多線程安全問題。
往期推薦
云計算到底是誰發明的?
長跑11年,騰訊開源的變與不變
低代碼發展專訪系列之一:低代碼平臺產品的使用者都是誰?
內容整理志愿者招募了!
點分享
點收藏
點點贊
點在看
總結
以上是生活随笔為你收集整理的高并发下的 HashMap 为什么会死循环的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ERP物理机迁移至阿里云实践
- 下一篇: 全球企业KVM开源贡献榜发布,腾讯云、华