HashMap為什麼執行緒不安全(hash碰撞與擴容導致)
一直以來都知道HashMap是執行緒不安全的,但是到底為什麼執行緒不安全,在多執行緒操作情況下什麼時候執行緒不安全?
讓我們先來了解一下HashMap的底層儲存結構,HashMap底層是一個Entry陣列,一旦發生Hash衝突的的時候,HashMap採用拉鍊法解決碰撞衝突,Entry內部的變數:
- final Object key;
- Object value;
- Entry next;
- int hash;
通過Entry內部的next變數可以知道使用的是連結串列,這時候我們可以知道,如果多個執行緒,在某一時刻同時操作HashMap並執行put操作,而有大於兩個key的hash值相同,如圖中a1、a2,這個時候需要解決碰撞衝突,而解決衝突的辦法上面已經說過,對於連結串列的結構在這裡不再贅述,暫且不討論是從連結串列頭部插入還是從尾部初入,這個時候兩個執行緒如果恰好都取到了對應位置的頭結點e1,而最終的結果可想而知,a1、a2兩個資料中勢必會有一個會丟失,如圖所示:
再來看下put方法
- public Object put(Object obj, Object obj1)
- {
- if(table == EMPTY_TABLE)
- inflateTable(threshold);
- if(obj == null)
- return putForNullKey(obj1);
- int i = hash(obj);
- int j = indexFor(i, table.length);
-
for(Entry entry = table[j]; entry != null; entry = entry.next)
- {
- Object obj2;
- if(entry.hash == i && ((obj2 = entry.key) == obj || obj.equals(obj2)))
- {
- Object obj3 = entry.value;
- entry.value = obj1;
- entry.recordAccess(this);
-
return obj3;
- }
- }
- modCount++;
- addEntry(i, obj, obj1, j);
- return null;
- }
put方法不是同步的,同時呼叫了addEntry方法:
- void addEntry(int i, Object obj, Object obj1, int j)
- {
- if(size >= threshold && null != table[j])
- {
- resize(2 * table.length);
- i = null == obj ? 0 : hash(obj);
- j = indexFor(i, table.length);
- }
- createEntry(i, obj, obj1, j);
- }
addEntry方法依然不是同步的,所以導致了執行緒不安全出現傷處問題,其他類似操作不再說明,原始碼一看便知,下面主要說一下另一個非常重要的知識點,同樣也是HashMap非執行緒安全的原因,我們知道在HashMap存在擴容的情況,對應的方法為HashMap中的resize方法:
- void resize(int i)
- {
- Entry aentry[] = table;
- int j = aentry.length;
- if(j == 1073741824)
- {
- threshold = 2147483647;
- return;
- } else
- {
- Entry aentry1[] = new Entry[i];
- transfer(aentry1, initHashSeedAsNeeded(i));
- table = aentry1;
- threshold = (int)Math.min((float)i * loadFactor, 1.073742E+009F);
- return;
- }
- }
可以看到擴容方法也不是同步的,通過程式碼我們知道在擴容過程中,會新生成一個新的容量的陣列,然後對原陣列的所有鍵值對重新進行計算和寫入新的陣列,之後指向新生成的陣列。
當多個執行緒同時檢測到總數量超過門限值的時候就會同時呼叫resize操作,各自生成新的陣列並rehash後賦給該map底層的陣列table,結果最終只有最後一個執行緒生成的新陣列被賦給table變數,其他執行緒的均會丟失。而且當某些執行緒已經完成賦值而其他執行緒剛開始的時候,就會用已經被賦值的table作為原始陣列,這樣也會有問題。