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:
通過這幾步,咱們會發現轉移的時候是逆序的。假如轉移前鏈表順序是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,天然是不合理的。