1. 程式人生 > >為什麼HashMap是執行緒不安全類?

為什麼HashMap是執行緒不安全類?

本文轉載自:http://blog.csdn.net/mydreamongo/article/details/8960667

一直以來只是知道HashMap是執行緒不安全的,但是到底HashMap為什麼執行緒不安全,多執行緒併發的時候在什麼情況下可能出現問題?

HashMap底層是一個Entry陣列,當發生hash衝突的時候,hashmap是採用連結串列的方式來解決的,在對應的陣列位置存放連結串列的頭結點。對連結串列而言,新加入的節點會從頭結點加入。

javadoc中關於hashmap的一段描述如下:

此實現不是同步的。如果多個執行緒同時訪問一個雜湊對映,而其中至少一個執行緒從結構上修改了該對映,則它必須

 保持外部同步。(結構上的修改是指新增或刪除一個或多個對映關係的任何操作;僅改變與例項已經包含的鍵關聯的值不是結構上的修改。)這一般通過對自然封裝該對映的物件進行同步操作來完成。如果不存在這樣的物件,則應該使用 Collections.synchronizedMap 方法來“包裝”該對映。最好在建立時完成這一操作,以防止對對映進行意外的非同步訪問,如下所示:

   Map m = Collections.synchronizedMap(new HashMap(...));

1、

  1. void addEntry(int hash, K key, V value, int bucketIndex) {  
  2.     Entry<K,V> e = table[bucketIndex];  
  3.         table[bucketIndex] = new Entry<K,V>(hash, key, value, e);  
  4.         if (size++ >= threshold)  
  5.             resize(2 * table.length);  
  6.     }  

在hashmap做put操作的時候會呼叫到以上的方法。現在假如A執行緒和B執行緒同時對同一個陣列位置呼叫addEntry,兩個執行緒會同時得到現在的頭結點,然後A寫入新的頭結點之後,B也寫入新的頭結點,那B的寫入操作就會覆蓋A的寫入操作造成A的寫入操作丟失

2、

  1. final Entry<K,V> removeEntryForKey(Object key) {  
  2.         int hash = (key == null) ? 0 : hash(key.hashCode());  
  3.         int i = indexFor(hash, table.length);  
  4.         Entry<K,V> prev = table[i];  
  5.         Entry<K,V> e = prev;  
  6.         while (e != null) {  
  7.             Entry<K,V> next = e.next;  
  8.             Object k;  
  9.             if (e.hash == hash &&  
  10.                 ((k = e.key) == key || (key != null && key.equals(k)))) {  
  11.                 modCount++;  
  12.                 size--;  
  13.                 if (prev == e)  
  14.                     table[i] = next;  
  15.                 else
  16.                     prev.next = next;  
  17.                 e.recordRemoval(this);  
  18.                 return e;  
  19.             }  
  20.             prev = e;  
  21.             e = next;  
  22.         }  
  23.         return e;  
  24.     }  

刪除鍵值對的程式碼如上:

當多個執行緒同時操作同一個陣列位置的時候,也都會先取得現在狀態下該位置儲存的頭結點,然後各自去進行計算操作,之後再把結果寫會到該陣列位置去,其實寫回的時候可能其他的執行緒已經就把這個位置給修改過了,就會覆蓋其他執行緒的修改

3、addEntry中當加入新的鍵值對後鍵值對總數量超過門限值的時候會呼叫一個resize操作,程式碼如下:

  1. void resize(int newCapacity) {  
  2.         Entry[] oldTable = table;  
  3.         int oldCapacity = oldTable.length;  
  4.         if (oldCapacity == MAXIMUM_CAPACITY) {  
  5.             threshold = Integer.MAX_VALUE;  
  6.             return;  
  7.         }  
  8.         Entry[] newTable = new Entry[newCapacity];  
  9.         transfer(newTable);  
  10.         table = newTable;  
  11.         threshold = (int)(newCapacity * loadFactor);  
  12.     }  

這個操作會新生成一個新的容量的陣列,然後對原陣列的所有鍵值對重新進行計算和寫入新的陣列,之後指向新生成的陣列。


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