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         }

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM