HashMap在多線程下不安全問題(JDK1.7)

本文分析是基於JDK1.7的HashMapjava

多線程狀況下HashMap所帶來的問題

  1. 多線程put操做後,get操做致使死循環。
  2. 多線程put操做,致使元素丟失。

死循環場景重現

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的擴容致使了死循環。數組

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. 假設咱們的hash算法是簡單的key mod一下表的大小(即數組的長度)。
  2. 最上面是old hash表,其中HASH表的size=2,因此key=3,5,7在mod 2 之後都衝突在table[1]這個位置上了。
  3. 接下來HASH表擴容,resize=4,而後全部的<key,value>從新進行散列分佈,過程以下:

在單線程狀況下,一切看起來都很美妙,擴容過程也至關順利。接下來看下併發狀況下的擴容。多線程

併發狀況下的擴容

  1. 有兩個線程,分別用紅色和藍色標註了。併發

  2. 在線程1執行到第5行代碼就被CPU調度掛起(執行完了,獲取到next是7),去執行線程2,且線程2把上面代碼都執行完畢。咱們來看看這個時候的狀態ide

  1. 接着CPU切換到線程1上來,執行4-12行代碼(已經執行完了第五行),首先安置健值爲3這個Entry:

注意::線程二已經完成執行完成,如今table裏面全部的Entry都是最新的,就是說7的next是3,3的next是null;如今第一次循環已經結束,3已經安置穩當。spa

  1. 看看接下來會發生什麼事情:
    • e=next=7;
    • e!=null,循環繼續
    • next=e.next=3
    • e.next 7的next指向3
    • 放置7這個Entry,如今如圖所示:

  1. 放置7以後,接着運行代碼:
    • e=next=3;
    • 判斷不爲空,繼續循環
    • next= e.next 這裏也就是3的next 爲null
    • e.next=7,就3的next指向7.
    • 放置3這個Entry,此時的狀態如圖

這個時候其實就出現了死循環了,3移動節點頭的位置,指向7這個Entry;在這以前7的next同時也指向了3這個Entry。線程

  1. 代碼接着往下執行,e=next=null,此時條件判斷會終止循環。此次擴容結束了。可是後續若是有查詢(不管是查詢的迭代仍是擴容),都會hang死在table[3]這個位置上。如今回過來看文章開頭的那個Demo,就是掛死在擴容階段的transfer這個方法上面。

出現問題的關鍵緣由:若是擴容前相鄰的兩個Entry在擴容後仍是分配到相同的table位置上,就會出現死循環的BUG。在複雜的生產環境中,這種狀況儘管不常見,可是可能會碰到。code

多線程put操做,致使元素丟失

下面來介紹下元素丟失的問題。此次咱們選取三、五、7的順序來演示:

  1. 若是在線程一執行到第5行代碼就被CPU調度掛起:

  1. 線程二執行完成:

  1. 這個時候接着執行線程一,首先放置7這個Entry:

  1. 再放置5這個Entry:

  1. 因爲5的next爲null,此時擴容動做結束,致使3這個Entry丟失。

JDK 8 的改進

JDK 8 中採用的是位桶 + 鏈表/紅黑樹的方式,當某個位桶的鏈表的長度超過 8 的時候,這個鏈表就將轉換成紅黑樹

HashMap 不會由於多線程 put 致使死循環(JDK 8 用 head 和 tail 來保證鏈表的順序和以前同樣;JDK 7 rehash 會倒置鏈表元素),可是還會有數據丟失等弊端(併發自己的問題)。所以多線程狀況下仍是建議使用 ConcurrentHashMap

爲何線程不安全

HashMap 在併發時可能出現的問題主要是兩方面:

  1. 若是多個線程同時使用 put 方法添加元素,並且假設正好存在兩個 put 的 key 發生了碰撞(根據 hash 值計算的 bucket 同樣),那麼根據 HashMap 的實現,這兩個 key 會添加到數組的同一個位置,這樣最終就會發生其中一個線程 put 的數據被覆蓋

  2. 若是多個線程同時檢測到元素個數超過數組大小 * loadFactor,這樣就會發生多個線程同時對 Node 數組進行擴容,都在從新計算元素位置以及複製數據,可是最終只有一個線程擴容後的數組會賦給 table,也就是說其餘線程的都會丟失,而且各自線程 put 的數據也丟失

相關文章
相關標籤/搜索