Java基礎知識強化之集合框架筆記80:HashMap的線程不安全性的體現

1. HashMap 的線程不安全性的體現:數組

主要是下面兩方面:安全

(1)多線程環境下,多個線程同時resize()時候,容易產生死鎖現象。即:resize死循環數據結構

(2)若是在使用迭代器的過程當中有其餘線程修改了map,那麼將拋出ConcurrentModificationException,即:fail-fast策略多線程

 

2. resize死循環:併發

(1)爲何會出現resize死循環函數

在單線程狀況下,只有一個線程對HashMap的數據結構進行操做,是不可能產生閉合的迴路的。那就只有在多線程併發的狀況下才會出現這種狀況,那就是在put操做的時候,若是size> initialCapacity * loadFactor,那麼這時候HashMap就會進行rehash操做,隨之HashMap的結構就會發生翻天覆地的變化。頗有可能就是在兩個線程在這個時候同時觸發了rehash操做,產生了閉合的迴路。this

(2)下面咱們從源碼中一步一步地分析resize死循環是如何產生的:spa

-->1. 存儲數據put(K key, V value)線程

public V put(K key, V value) { ...... //算Hash值
    int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); //若是該key已被插入,則替換掉舊的value (連接操做)
    for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; //該key不存在,須要增長一個結點
  addEntry(hash, key, value, i); return null; }

當咱們往HashMap中put元素的時候,先根據key的hash值獲得這個元素在數組中的位置(即下標),而後就能夠把這個元素放到對應的位置中了。 若是這個元素所在的位置上已經存放有其餘元素了,那麼在同一個位子上的元素將以鏈表的形式存放,新加入的放在鏈頭,而先前加入的放在鏈尾code

 

-->2. 檢查容量是否超標addEntry

void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<K,V>(hash, key, value, e); //查看當前的size是否超過了咱們設定的閾值threshold,若是超過,須要resize
    if (size++ >= threshold) resize(2 * table.length); }

能夠看到,若是如今size已經超過了threshold,那麼就要進行resize操做,新建一個更大尺寸的hash表,而後把數據從老的Hash表中遷移到新的Hash表中。

 

-->3. 調整Hash表大小resize

void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; ...... //建立一個新的Hash Table
    Entry[] newTable = new Entry[newCapacity]; //將Old Hash Table上的數據遷移到New Hash Table上
 transfer(newTable); table = newTable; threshold = (int)(newCapacity * loadFactor); }

當table[]數組容量較小,容易產生哈希碰撞,因此,Hash表的尺寸和容量很是的重要。通常來講,Hash表這個容器當有數據要插入時,都會檢查容量有沒有超過設定的thredhold,若是超過,須要增大Hash表的尺寸,這個過程稱爲resize。

多個線程同時往HashMap添加新元素時,屢次resize會有必定機率出現死循環,由於每次resize須要把舊的數據映射到新的哈希表,多個線程執行resize操做都須要得到舊哈希表資源就會有概率出現死鎖現象,而後相互等待,從而造成死循環。這一部分代碼在HashMap#transfer() 方法,以下:

void transfer(Entry[] newTable) { Entry[] src = table; int newCapacity = newTable.length; //下面這段代碼的意思是: // 從OldTable裏摘一個元素出來,而後放到NewTable中
    for (int j = 0; j < src.length; j++) { Entry<K,V> e = src[j]; if (e != null) { src[j] = null; 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); } } }

大概看下transfer:

  • 對索引數組中的元素遍歷
  • 對鏈表上的每個節點遍歷:用 next 取得要轉移那個元素的下一個,將 e 轉移到新 Hash 表的頭部,使用頭插法插入節點
  • 循環2,直到鏈表節點所有轉移
  • 循環1,直到全部索引數組所有轉移

通過這幾步,咱們會發現轉移的時候是逆序的。假如轉移前鏈表順序是1->2->3,那麼轉移後就會變成3->2->1。這時候就有點頭緒了.

死鎖問題不就是由於1->2的同時2->1形成的嗎?

因此,HashMap 的死鎖問題就出在這個transfer()函數上

 

黃色部分代碼是致使多線程使用hashmap出現CPU使用率驟增,從而多個線程阻塞的罪魁禍首

 

(3)總結:

   當多個線程同時檢測到總數量超過門限值的時候就會同時調用resize操做,各自生成新的數組並rehash後賦給該map底層的數組table,結果最終只有最後一個線程生成的新數組被賦給table變量,其餘線程的均會丟失。並且當某些線程已經完成賦值而其餘線程剛開始的時候,就會用已經被賦值的table做爲原始數組,這樣也會有問題。

 

 

3. fail-fast策略:

若是在使用迭代器的過程當中有其餘線程修改了map,那麼將拋出ConcurrentModificationException,這就是所謂fail-fast策略

首先咱們要知道迭代器是依附於對應的Map的,好比說:

在線程Thread1中,咱們得到map的迭代器iterator1,此時使用iterator1遍歷map數據(Thread1運行中,沒有結束)

在線程Thread2中,咱們修改了map,map變成了map1,好比時候添加元素(put)、或者刪除元素(remove)。此時Thread1就會拋出ConcurrentModificationException錯誤。

修改了map,咱們迭代的map不是原來的map,可是竟然仍是使用依附原來的map的迭代器iterator1,去迭代map1,天然是不合理的。

相關文章
相關標籤/搜索