HashMap底層是一個Entry數組,當發生hash沖突的時候,hashmap是采用鏈表的方式來解決的,在對應的數組位置存放鏈表的頭結點。對鏈表而言,新加入的節點會從頭結點加入。
我們來分析一下多線程訪問:
1.在hashmap做put操作的時候會調用下面方法:
// 新增Entry。將“key-value”插入指定位置,bucketIndex是位置索引。 void addEntry(int hash, K key, V value, int bucketIndex) { // 保存“bucketIndex”位置的值到“e”中 Entry<K,V> e = table[bucketIndex]; // 設置“bucketIndex”位置的元素為“新Entry”, // 設置“e”為“新Entry的下一個節點” table[bucketIndex] = new Entry<K,V>(hash, key, value, e); // 若HashMap的實際大小 不小於 “閾值”,則調整HashMap的大小 if (size++ >= threshold) resize(2 * table.length); }
在hashmap做put操作的時候會調用到以上的方法。現在假如A線程和B線程同時對同一個數組位置調用addEntry,兩個線程會同時得到現在的頭結點,然后A寫入新的頭結點之后,B也寫入新的頭結點,那B的寫入操作就會覆蓋A的寫入操作造成A的寫入操作丟失
2.刪除鍵值對的代碼
<span style="font-size: 18px;"> </span>// 刪除“鍵為key”的元素 final Entry<K,V> removeEntryForKey(Object key) { // 獲取哈希值。若key為null,則哈希值為0;否則調用hash()進行計算 int hash = (key == null) ? 0 : hash(key.hashCode()); int i = indexFor(hash, table.length); Entry<K,V> prev = table[i]; Entry<K,V> e = prev; // 刪除鏈表中“鍵為key”的元素 // 本質是“刪除單向鏈表中的節點” while (e != null) { Entry<K,V> next = e.next; Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { modCount++; size--; if (prev == e) table[i] = next; else prev.next = next; e.recordRemoval(this); return e; } prev = e; e = next; } return e; }
當多個線程同時操作同一個數組位置的時候,也都會先取得現在狀態下該位置存儲的頭結點,然后各自去進行計算操作,之后再把結果寫會到該數組位置去,其實寫回的時候可能其他的線程已經就把這個位置給修改過了,就會覆蓋其他線程的修改。
3.addEntry中當加入新的鍵值對后鍵值對總數量超過門限值的時候會調用一個resize操作,代碼如下:
// 重新調整HashMap的大小,newCapacity是調整后的容量 void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; //如果就容量已經達到了最大值,則不能再擴容,直接返回 if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } // 新建一個HashMap,將“舊HashMap”的全部元素添加到“新HashMap”中, // 然后,將“新HashMap”賦值給“舊HashMap”。 Entry[] newTable = new Entry[newCapacity]; transfer(newTable); table = newTable; threshold = (int)(newCapacity * loadFactor); }
這個操作會新生成一個新的容量的數組,然后對原數組的所有鍵值對重新進行計算和寫入新的數組,之后指向新生成的數組。
當多個線程同時檢測到總數量超過門限值的時候就會同時調用resize操作,各自生成新的數組並rehash后賦給該map底層的數組table,結果最終只有最后一個線程生成的新數組被賦給table變量,其他線程的均會丟失。而且當某些線程已經完成賦值而其他線程剛開始的時候,就會用已經被賦值的table作為原始數組,這樣也會有問題。
