Java基礎:詳解HashMap在多線程下不安全


今天想知道HashMap為什么在多線程下不安全,找了許多資料,終於理解了。

首先先了解一下HashMap:

HashMap實現的原理是:數組+鏈表

 

HashMap的size大於等於(容量*加載因子)的時候,會觸發擴容的操作,這個是個代價不小的操作。 

為什么要擴容呢?

HashMap默認的容量是16,隨着元素不斷添加到HashMap里,出現hash沖突的機率就更高,那每個桶對應的鏈表就會更長, 

這樣會影響查詢的性能,因為每次都需要遍歷鏈表,比較對象是否相等,一直到找到元素為止。

為了提升查詢性能,只能擴容,減少hash沖突,讓元素的key盡量均勻的分布。

在單線程中,HashMap是安全的,但是在高並發的環境下,會出現不安全,原因在於HashMap的擴容。

我們先看下HashMap擴容的代碼:

void resize(int newCapacity) {  
        Entry[] oldTable = table;  
        int oldCapacity = oldTable.length;  
        if (oldCapacity == MAXIMUM_CAPACITY) {  
            threshold = Integer.MAX_VALUE;  
            return;  
        }  
  
        Entry[] newTable = new Entry[newCapacity];  
          
        transfer(newTable);//可能導致環鏈  
          
        table = newTable;  
        threshold = (int)(newCapacity * loadFactor);  
}  

  

transfer方法就是進行HashMap的擴容的核心方法:

void transfer(Entry[] newTable) {  
    Entry[] src = table;  
    int newCapacity = newTable.length;  
    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);  
        }  
    }  
}  

在並發情況下進行擴容,有一個線程執行到

Entry<K,V> next = e.next;  

而另外一個線程已經執行完擴容,再等這個線程執行完就會出現環路,並且也會丟失一些節點。

我查看一下陳皓大神的文章,里面寫的很詳細:

https://coolshell.cn/articles/9606.html

 

正常的ReHash的過程

畫了個圖做了個演示。

  • 我假設了我們的hash算法就是簡單的用key mod 一下表的大小(也就是數組的長度)。
  • 最上面的是old hash 表,其中的Hash表的size=2, 所以key = 3, 7, 5,在mod 2以后都沖突在table[1]這里了。
  • 接下來的三個步驟是Hash表 resize成4,然后所有的<key,value> 重新rehash的過程

並發下的Rehash

1)假設我們有兩個線程。我用紅色和淺藍色標注了一下。

我們再回頭看一下我們的 transfer代碼中的這個細節:

1
2
3
4
5
6
7
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 );

而我們的線程二執行完成了。於是我們有下面的這個樣子。

注意,因為Thread1的 e 指向了key(3),而next指向了key(7),其在線程二rehash后,指向了線程二重組后的鏈表。我們可以看到鏈表的順序被反轉后。

2)線程一被調度回來執行。

  • 先是執行 newTalbe[i] = e;
  • 然后是e = next,導致了e指向了key(7),
  • 而下一次循環的next = e.next導致了next指向了key(3)

3)一切安好。

線程一接着工作。把key(7)摘下來,放到newTable[i]的第一個,然后把e和next往下移

4)環形鏈接出現。

e.next = newTable[i] 導致  key(3).next 指向了 key(7)

注意:此時的key(7).next 已經指向了key(3), 環形鏈表就這樣出現了。

於是,當我們的線程一調用到,HashTable.get(11)時,悲劇就出現了——Infinite Loop。



免責聲明!

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



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