hashmap擴容時死循環問題
源碼如下 ——–Put一個Key,Value對到Hash表中:
public V put(K key, V value) { ...... //計算Hash值 int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); //各種校驗吧 for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; //該key不存在,需要增加一個結點 addEntry(hash, key, value, i); return null; }
這里添加一個節點需要檢查是否超出容量,出現了一個負載因子。
void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<K,V>(hash, key, value, e); //查看當前的size是否超過了我們設定的閾值threshold,如果超過,需要resize if (size++ >= threshold) resize(2 * table.length);//擴容都是2倍2倍的來的, }
1.奇數不行的解釋很能被接受,在計算hash的時候,確定落在數組的位置的時候,計算方法是(n - 1) & hash ,奇數n-1為偶數,偶數2進制的結尾都是0,經過&運算末尾都是0,會 增加hash沖突。
2.為啥要是2的冪,不能是2的倍數么,比如6,10? 2.1 hashmap 結構是數組,每個數組里面的結構是node(鏈表或紅黑樹),正常情況下,如果你想放數據到不同的位置,肯定會想到取余數確定放在那個數據里,
計算公式: hash % n,這個是十進制計算。在計算機中, (n - 1) & hash,當n為2次冪時,會滿足一個公式:(n - 1) & hash = hash % n,計算更加高效。
2.2 只有是2的冪數的數字經過n-1之后,二進制肯定是 ...11111111 這樣的格式,這種格式計算的位置的時候,完全是由產生的hash值類決定,而不受n-1(組數長度) 影響。
你可能會想, 受影響不是更好么,又計算了一下,類似於擾動函數,hash沖突可能更低了,這里要考慮到擴容了,2的冪次方*2,在二進制中比如4和8,代表2的2次方和3次方,他們的2進制結構相 似,
比如 4和8 00000100 0000 1000 只是高位向前移了一位,這樣擴容的時候,只需要判斷高位hash,移動到之前位置的倍數就可以了,免去了重新計算位置的運算。
既然新建了一個更大尺寸的hash表,然后把數據從老的Hash表中遷移到新的Hash表中。
void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; ...... //創建一個新的Hash Table Entry[] newTable = new Entry[newCapacity]; //將Old Hash Table上的數據遷移到New Hash Table上 transfer(newTable); table = newTable; threshold = (int)(newCapacity * loadFactor); }
好,重點在這里面的transfer()!
void transfer(Entry[] newTable) { Entry[] src = table; int newCapacity = newTable.length; //下面這段代碼的意思是: // 從OldTable里摘一個元素出來,然后放到NewTable中 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); } } }
假設有一個hashMap數組(正常是2的N次長度,這里方便舉例), 節點3上存有abc元素,此時發生擴容
此時假設有兩個線程

線程B在執行到Entry<K,V> next = e.next;后掛起,此時e指向元素a,e.next指向元素b
到線程A在new table的數組7位置依次用頭插法插入3個元素后
此時線程B繼續執行以下代碼
Entry<K,V> next = e.next; //next = b
e.next = newTable[i]; //
將
數組7的地址賦予變量e.next
newTable[i] = e; //
將a放到數組7的位置
e = next; // e = next = b
執行結束的關系如圖

變量e = b不是null,循環繼續執行,
Entry<K,V> next = e.next; // next = a
e.next = newTable[i]; //數組7地址指向e.next newTable[i] = e; //將b放到數組7的位置 e = next; //e =next = a
執行后引用關系圖
此時變量e = a仍舊不為空,繼續循環。。
Entry<K,V> next = e.next; // 變量a沒有next,所以next = null
e.next = newTable[i]; // 因為
newTable[i]存的是b,這一步相當於將a的next指向了b,於是問題出現了
newTable[i] = e; //將變量a放到數組7的位置
e
= next; // e= next = null

當在數組7遍歷節點尋找對應的key時, 節點a和b就發生了死循環, 直到cpu被完全耗盡。
另外,如果最終線程2執行了table = newTable;那元素C就發生了數據丟失問題
四.總結
通過解讀HashMap源碼並結合實例可以發現,HashMap擴容導致死循環的主要原因在於擴容過程中使用頭插法將oldTable中的單鏈表中的節點插入到newTable的單鏈表頭中,所以newTable中的單鏈表會倒置oldTable中的單鏈表。那么在多個線程同時擴容的情況下就可能導致擴容后的HashMap中存在一個有環的單鏈表,從而導致后續執行get操作的時候,會觸發死循環,引起CPU的100%問題。所以一定要避免在並發環境下使用HashMap。