HashMap執行緒不安全的表現 -- Java 8
HashMap執行緒不安全的表現 -- Java 8
先來看看HashMap.put方法的原始碼
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) //如果該位置為null,說明沒有雜湊衝突,直接插入 --------------------(1) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
如果有兩個執行緒A和B,都進行插入資料,剛好這兩條不同的資料經過雜湊計算後得到的雜湊碼是一樣的,且該位置還沒有其他的資料。所以這兩個執行緒都會進入我在上面標記為1的程式碼中。假設一種情況,執行緒A通過if判斷,該位置沒有雜湊衝突,進入了if語句,還沒有進行資料插入,這時候CPU就把資源讓給了執行緒B,執行緒A停在了if語句裡面,執行緒B判斷該位置沒有雜湊衝突(執行緒A的資料還沒插入),也進入了if語句,執行緒B執行完後,輪到執行緒A執行,現線上程A直接在該位置插入而不用再判斷。這時候,你會發現執行緒A把執行緒B插入的資料給覆蓋了。發生了執行緒不安全情況。本來在HashMap中,發生雜湊衝突是可以用連結串列法或者紅黑樹來解決的,但是在多執行緒中,可能就直接給覆蓋了。
上面所說的是一個圖來解釋可能更加直觀。如下面所示,兩個執行緒在同一個位置新增資料,後面新增的資料就覆蓋住了前面新增的。
發生在連結串列處插入資料發生執行緒不安全的情況也相似。
如兩個執行緒都在遍歷到最後一個節點,都要在最後新增一個數據,那麼後面新增資料的執行緒就會把前面新增的資料給覆蓋住。
我能想到的其他情況:在擴容的時候插入資料,有可能會把新插入的覆蓋住;在擴容的時候刪除資料,會刪除不了。
下面是擴容方法resize()的部分程式碼
如果我在擴容時,在資料從舊陣列複製到新陣列過程中,這時候某個執行緒插入一條資料,這時候是插入到新陣列中,但是在資料複製過程中,HashMap是沒有檢查新陣列上的位置是否為空,所以新插入的資料會被後面從舊陣列中複製過來的資料覆蓋住。
如果在(2)剛執行後,某個執行緒就立刻想刪除以前插入的某個元素,你會發現刪除不了,因為table指向了新陣列,而這時候新陣列還沒有資料。
1 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//----------------------(1) 2 table = newTab;// ------------------------(2) 3 if (oldTab != null) { 4 for (int j = 0; j < oldCap; ++j) { 5 Node<K,V> e; 6 if ((e = oldTab[j]) != null) { 7 oldTab[j] = null; 8 if (e.next == null) 9 newTab[e.hash & (newCap - 1)] = e; //---------------在新陣列上插入陣列都不會檢查該位置是否為null 10 else if (e instanceof TreeNode) 11 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); 12 else { // preserve order 13 Node<K,V> loHead = null, loTail = null; 14 Node<K,V> hiHead = null, hiTail = null; 15 Node<K,V> next; 16 do { 17 next = e.next; 18 if ((e.hash & oldCap) == 0) { 19 if (loTail == null) 20 loHead = e; 21 else 22 loTail.next = e; 23 loTail = e; 24 } 25 else { 26 if (hiTail == null) 27 hiHead = e; 28 else 29 hiTail.next = e; 30 hiTail = e; 31 } 32 } while ((e = next) != null); 33 if (loTail != null) { 34 loTail.next = null; 35 newTab[j] = loHead; //-------------------在新陣列上插入陣列都不會檢查該位置是否為null 36 } 37 if (hiTail != null) { 38 hiTail.next = null; 39 newTab[j + oldCap] = hiHead; //------------在新陣列上插入陣列都不會檢查該位置是否為null 40 } 41 } 42 } 43 } 44 }