一、背景
HashMap的擴容機制就是重新申請一個容量是當前容量2倍的桶數組,然后將舊數組中的元素逐個映射到新的數組里邊,然后將原先的桶逐個置為引用失效。HashMap之所以線程不安全,就是resize這里出的問題。
二、線程不安全場景
1、put的時候導致的多線程數據不一致。
這個問題比較好想象,比如有兩個線程A和B,首先A希望先插入一個key-value到HashMap中,首先計算key-value索引位置,然后獲取到該桶里面鏈表的頭節(jié)點,并且key不存在桶中,需要新建節(jié)點插入,此時線程A的時間片用完了;線程B被調度得以執(zhí)行,和線程A一樣執(zhí)行,但線程B比較幸運,成功的將key-vaule插入到了鏈表中,隨后線程A再次被調度運行時,它依然持有過期的鏈表頭但它對此一無所知,如此一來就覆蓋了線程B插入的記錄,但線程B插入的記錄就憑空消失了。
void createEntry(int hash, K key, V value, int bucketIndex) {
// 1. 把table中該位置原來的Entry保存
Entry<K,V> e = table[bucketIndex];
// 2. 在table中該位置新建一個Entry:將原頭結點位置(數組上)的鍵值對 放入到(鏈表)后1個節(jié)點中、將需插入的鍵值對 放入到頭結點中(數組上)-> 從而形成鏈表
// 即 在插入元素時,是在鏈表頭插入的,table中的每個位置永遠只保存最新插入的Entry,舊的Entry則放入到鏈表中(即 解決Hash沖突)
table[bucketIndex] = new Entry<>(hash, key, value, e);
// 3. 哈希表的鍵值對數量計數增加
size++;
}
2、HashMap的get操作死循環(huán)
達到resize條件,兩個線程各自進行resize的第一步-擴容。讓我們回顧一下ReHash源碼
//resieze的核心源碼如下
//這個方法的功能時將原來的記錄重新計算在新桶的位置,然后遷移過去
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()過程中,在將舊數組上的數據 轉移到 新數組上時,轉移數據操作 = 按舊鏈表的正序遍歷鏈表、在新鏈表的頭部依次插入,即在轉移數據、擴容后,容易出現(xiàn)鏈表逆序的情況
此時若(多線程)并發(fā)執(zhí)行 put()操作,一旦出現(xiàn)擴容情況,則 容易出現(xiàn) 環(huán)形鏈表,從而在獲取數據、遍歷鏈表時 形成死循環(huán)(Infinite Loop),即 死鎖的狀態(tài)。


對于線程B來說e=Entry3,next=Entry2;
這時候線程A暢通無阻的進行著Rehash,當ReHash完成后,結果如下

接下來線程B恢復,繼續(xù)執(zhí)行屬于它自己的ReHash,線程B剛才的狀態(tài)是e = Entry3,next=Entry2
當執(zhí)行紅框內的一行代碼時

顯然i = 3,因為剛才線程A對于Entry3的hash結果也是3.
我們繼續(xù)執(zhí)行

Entry3放入線程B的數組下標為3的位置,并且e指向了Entry2。
接著新一輪的循環(huán),又執(zhí)行到

e = Entry2
next = Entry3

接下來執(zhí)行

用頭插法把Entry2插入到線程B的數組的頭節(jié)點

接下來執(zhí)行紅框內的代碼

e = Entry3
next = null
當我們執(zhí)行下面這一行的時候,見證奇跡的時刻來臨了:

newTable[i] = Entry2
e = Entry3
Entry2.next = Entry3
Entry3.next = Entry2
鏈表出現(xiàn)了環(huán)形!
