本文分析是基於JDK1.7的HashMapjava
public class HashMapTest extends Thread {
private static HashMap<Integer, Integer> map = new HashMap<>(2);
private static AtomicInteger at = new AtomicInteger();
@Override
public void run() {
while (at.get() < 1000000) {
map.put(at.get(), at.get());
at.incrementAndGet();
}
}
public static void main(String[] args) {
HashMapTest t0 = new HashMapTest();
HashMapTest t1 = new HashMapTest();
HashMapTest t2 = new HashMapTest();
HashMapTest t3 = new HashMapTest();
HashMapTest t4 = new HashMapTest();
HashMapTest t5 = new HashMapTest();
t0.start();
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
for (int i = 0; i < 1000000; i++) {
Integer integer = map.get(i);
System.out.println(integer);
}
}
}
複製代碼
反覆執行幾回,出現這種狀況則表示死循環了:算法
由上可知,Thread-7因爲HashMap的擴容致使了死循環。數組
1 void transfer(Entry[] newTable, boolean rehash) {
2 int newCapacity = newTable.length;
3 for (Entry<K,V> e : table) {
4 while(null != e) {
5 Entry<K,V> next = e.next;
6 if (rehash) {
7 e.hash = null == e.key ? 0 : hash(e.key);
8 }
9 int i = indexFor(e.hash, newCapacity);
10 e.next = newTable[i];
11 newTable[i] = e;
12 e = next;
13 }
14 }
15 }
複製代碼
咱們先來看下單線程狀況下,正常的rehash過程:安全
在單線程狀況下,一切看起來都很美妙,擴容過程也至關順利。接下來看下併發狀況下的擴容。多線程
有兩個線程,分別用紅色和藍色標註了。併發
在線程1執行到第5行代碼就被CPU調度掛起(執行完了,獲取到next是7),去執行線程2,且線程2把上面代碼都執行完畢。咱們來看看這個時候的狀態ide
注意::線程二已經完成執行完成,如今table裏面全部的Entry都是最新的,就是說7的next是3,3的next是null;如今第一次循環已經結束,3已經安置穩當。spa
這個時候其實就出現了死循環了,3移動節點頭的位置,指向7這個Entry;在這以前7的next同時也指向了3這個Entry。線程
出現問題的關鍵緣由:若是擴容前相鄰的兩個Entry在擴容後仍是分配到相同的table位置上,就會出現死循環的BUG。在複雜的生產環境中,這種狀況儘管不常見,可是可能會碰到。code
下面來介紹下元素丟失的問題。此次咱們選取三、五、7的順序來演示:
JDK 8 中採用的是位桶 + 鏈表/紅黑樹的方式,當某個位桶的鏈表的長度超過 8 的時候,這個鏈表就將轉換成紅黑樹
HashMap 不會由於多線程 put 致使死循環(JDK 8 用 head 和 tail 來保證鏈表的順序和以前同樣;JDK 7 rehash 會倒置鏈表元素),可是還會有數據丟失等弊端(併發自己的問題)。所以多線程狀況下仍是建議使用 ConcurrentHashMap
HashMap 在併發時可能出現的問題主要是兩方面:
若是多個線程同時使用 put 方法添加元素,並且假設正好存在兩個 put 的 key 發生了碰撞(根據 hash 值計算的 bucket 同樣),那麼根據 HashMap 的實現,這兩個 key 會添加到數組的同一個位置,這樣最終就會發生其中一個線程 put 的數據被覆蓋
若是多個線程同時檢測到元素個數超過數組大小 * loadFactor,這樣就會發生多個線程同時對 Node 數組進行擴容,都在從新計算元素位置以及複製數據,可是最終只有一個線程擴容後的數組會賦給 table,也就是說其餘線程的都會丟失,而且各自線程 put 的數據也丟失