多線程環境下操做HashMap的問題

HashMap爲何不是線程安全,併發操做Hashmap會帶來什麼問題:
這個問題曾經有一個面試官問過我,當時我天真的覺得是讀寫操做併發時存在髒數據的問題,當時面試官不置能否。我後面回來查資料,發現沒有那麼簡單。併發操做HashMap,是有可能帶來死循環以及數據丟失的問題的。html

具體狀況以下:(如下代碼轉自美團點評技術團隊的文章Java8系列之從新認識HashMap)java

情景以下代碼:面試

 

 
  1. public class HashMapInfiniteLoop { 數組

  2.  
  3. private static HashMap<Integer,String> map = new HashMap<Integer,String>(2,0.75f); 安全

  4. public static void main(String[] args) { 併發

  5. map.put(5, "C"); 高併發

  6.  
  7. new Thread("Thread1") { oop

  8. public void run() { 性能

  9. map.put(7, "B"); .net

  10. System.out.println(map);

  11. };

  12. }.start();

  13. new Thread("Thread2") {

  14. public void run() {

  15. map.put(3, "A);

  16. System.out.println(map);

  17. };

  18. }.start();

  19. }

  20. }

 

其中,map初始化爲一個長度爲2的數組,loadFactor=0.75,threshold=2*0.75=1,也就是說當put第二個key的時候,map就須要進行擴容。

考慮這樣一種狀況:
先放出transfer的部分代碼:

 

 
  1. do {

  2. Entry<K,V> next = e.next; //假設線程一執行到這裏就被調度掛起了

  3. int i = indexFor(e.hash, newCapacity);

  4. e.next = newTable[i];

  5. newTable[i] = e;

  6. e = next;

  7. } while (e != null);

線程一、線程2都添加了數據以後,線程1執行到transfer()方法的第一行就被調度掛起了,這時線程2被調度來執行擴容操做。線程2的擴容操做結束以後,線程1被調度回來繼續執行,此時因爲線程2的執行,e已經指向了線程2修改以後的反轉鏈表,可是線程1並不知道線程2已經在它以前作過這些操做了,因而它繼續往下走,此時next=key(7),

而後計算索引。索引計算完以後執行e.next=newTable[i],此時e.next=key(7)。繼續往下走,newTable[i]=e,此時newTable[i]=key(3),再往下,e=next,此時e指向了key(7),本次循環結束。從線程二重組鏈表結束,到線程1第一輪循環結束的變化圖以下:

一切看起來都尚未什麼問題。而後新一輪循環開始

這一輪循環咱們不須要走完,就能發現問題。

第一句,執行後爲:next=null;

第二句,計算索引,仍是i

第三句,在這裏就出問題了,這句話執行的是e.next=newTable[i],咱們看上圖,newTable[i]指向的是key(3),所以出現鏈表末尾的元素的next指針指向了鏈表頭,循環鏈表就出現了。(按道理,HashMap是不存在循環鏈表的。)

第四句話,將鏈表頭的元素換成key(7),而循環鏈表依然存在。

第五句,e=null,執行到這循環結束,由於e=null了。

整個過程並不會發生明顯的異常。看起來一切安好。順利的完成了rehash,可是悲劇在後面:當咱們調用get()這個鏈表中不存在的元素的時候,就會出現死循環。go die

一句話總結就是,併發環境下的rehash過程可能會帶來循環鏈表,致使死循環導致線程掛掉。

所以併發環境下,建議使用Java.util.concurrent包中的ConcurrentHashMap以保證線程安全。

至於HashTable,它並未使用分段鎖,而是鎖住整個數組,高併發環境下效率很是的低,會致使大量線程等待。 一樣的,Synchronized關鍵字、Lock性能都不如分段鎖實現的ConcurrentHashMap。

相關文章
相關標籤/搜索