HashMap爲何不是線程安全,併發操做Hashmap會帶來什麼問題:
這個問題曾經有一個面試官問過我,當時我天真的覺得是讀寫操做併發時存在髒數據的問題,當時面試官不置能否。我後面回來查資料,發現沒有那麼簡單。併發操做HashMap,是有可能帶來死循環以及數據丟失的問題的。html
具體狀況以下:(如下代碼轉自美團點評技術團隊的文章Java8系列之從新認識HashMap)java
情景以下代碼:面試
public class HashMapInfiniteLoop {
數組
private static HashMap<Integer,String> map = new HashMap<Integer,String>(2,0.75f);
安全
public static void main(String[] args) {
併發
map.put(5, "C");
高併發
new Thread("Thread1") {
oop
public void run() {
性能
map.put(7, "B");
.net
System.out.println(map);
};
}.start();
new Thread("Thread2") {
public void run() {
map.put(3, "A);
System.out.println(map);
};
}.start();
}
}
其中,map初始化爲一個長度爲2的數組,loadFactor=0.75,threshold=2*0.75=1,也就是說當put第二個key的時候,map就須要進行擴容。
考慮這樣一種狀況:
先放出transfer的部分代碼:
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);
線程一、線程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。