目錄
1.併發add相同hash值or相同key的元素,導致丟失
Jdk1.7源碼
void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) {//擴容 resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } //將新增的元素放到i位置,並將它的next指向舊的元素 Entry<K,V> e = table[bucketIndex];//當前i位置的元素 table[bucketIndex] = new Entry<>(hash, key, value, e); size++; } |
當A、B兩個線程併發add相同hash值元素時候,當A、B都執行完畢黃色部分後,A、B獲取的e都爲otherValue。
假設A快一步執行綠色部分,此時內存中存儲如下:
下一步,B執行綠色部分,此時內存中存儲如下:
然後就是B線程的值覆蓋了A線程的,導致A線程add的值丟失。
原文:https://blog.csdn.net/lan861698789/article/details/81697398
2. Rehash導致死循環
在擴容時會做一個rehash的操作,這裏會導致死循環。
Jdk1.7 Rehash代碼:
//對老數據進行重新計算hash值,重新做數據下標映射, //然後全部複製到新數組 void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length;
Entry[] newTable = new Entry[newCapacity]; transfer(newTable, initHashSeedAsNeeded(newCapacity)); table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); } void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; for (Entry<K,V> e : table) { while(null != e) { Entry<K,V> next = e.next; if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } } } |
resize步驟如下:
1.對數組table中的元素遍歷
2.對鏈表上的每一個節點遍歷:用 next 取得要轉移那個元素的下一個,將 e 轉移到新 Hash 表的頭部,使用頭插法插入節點。
3.循環2,直到鏈表節點全部轉移
4.循環1,直到所有索引數組全部轉移
經過這幾步,我們會發現轉移的時候是逆序的。假如轉移前鏈表順序是1->2->3,那麼轉移後就會變成3->2->1。這時候就有點頭緒了,死鎖問題不就是因爲1->2的同時2->1造成的嗎?所以,HashMap 的死鎖問題就出在這個transfer()函數上。
先演示單線程
單線程情況下,rehash 不會出現任何問題:
假設hash算法就是最簡單的 key mod table.length(也就是數組的長度)。
最上面的是old hash 表,其中的Hash表的 size = 2, 所以 key = 3, 7, 5,在 mod 2以後碰撞發生在 table[1]
接下來的三個步驟是 Hash表 resize 到4,並將所有的 <key,value> 重新rehash到新 Hash 表的過程
如圖所示:
多線程時候
爲了思路更清晰,我們只將關鍵代碼展示出來
void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; for (Entry<K,V> e : table) { while(null != e) { Entry<K,V> next = e.next; if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } } } |
假設A、B兩個線程都執行resize(),然後都執行完了紅色部分,這時候兩個的e都是key3,next都是key7.
然後A讓出cpu,B全部執行完成,此時內存數據結構如下:
此時線程A的key還是key3,next還是key7。(而key7的next卻是key3了)
線程A繼續執行,插入table[i],next指向以前的table[i],也就是key7
內存結構圖如下:
通過圖我們發現,此時進入死循環了。