從源碼分析JDK8對HashMap死循環的修復

引言

上一篇咱們分析了HashMap的put和get方法,大家應該對HashMap有個簡單的瞭解,如果不瞭解得可以看上一篇文章。本文分析的重點是resize擴容方法,因爲JDK8在這段代碼上面對JDK7做了修改,現在我們一起來看看具體做了哪些修改,以及爲什麼要這麼修改。

1、分析JDK7源碼

新建一個更大尺寸的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);
}

下面是遷移的代碼:

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是非線程安全的,上面這段代碼在多線程的情況下會導致HashMap死循環,我們來演示下這個過程,最上面的是old hash 表,其中的Hash表的size=2, 所以key = 3, 7, 5,現在要進行擴容了,假設我們有兩個線程,線程二執行完成了:
在這裏插入圖片描述
線程一現在開始執行,先是執行 newTalbe[i] = e,然後是e = next,導致了e指向了key(7),而下一次循環的next = e.next導致了next指向了key(3)
在這裏插入圖片描述
再看下一流程,也正常
在這裏插入圖片描述
環形鏈接出現,e.next = newTable[i] 導致 key(3).next 指向了 key(7)
注意:此時的key(7).next 已經指向了key(3), 環形鏈表就這樣出現了。
在這裏插入圖片描述

2、分析JDK8源碼

由於resize的方法比較長,我先說下具體的思路,然後再貼源碼,大家就更加清楚了:

  • 如果table == null, 則初始化threshold值, 生成空table返回
  • 如果table不爲空, 需要重新計算table的長度, newCap= oldCap<< 1(擴容爲原來的2倍, 如果原oldCap已經到了上限, 則newCap= oldCap)
  • 遍歷oldTable,首節點不爲空則進入下一步,爲空則本次循環結束。
  • 無後續節點, 重新計算hash位, 並賦值,本次循環結束
  • 當前是紅黑樹, 走紅黑樹的重定位
  • 當前是鏈表, JAVA7時還需要重新計算hash位, 但是JAVA8做了優化, 通過(e.hash & oldCap) == 0來判斷是否需要移位; 如果爲真則在原位不動, 否則則需要移動到當前hash槽位 + oldCap的位置
final Node<K,V>[] resize() {
	Node<K,V>[] oldTab = table;
	int oldCap = (oldTab == null) ? 0 : oldTab.length;
	int oldThr = threshold;
	int newCap, newThr = 0;
	if (oldCap > 0) {
		if (oldCap >= MAXIMUM_CAPACITY) {
			threshold = Integer.MAX_VALUE;
			return oldTab;
		}
		else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
				 oldCap >= DEFAULT_INITIAL_CAPACITY)
			newThr = oldThr << 1; // double threshold
	}
	else if (oldThr > 0) // initial capacity was placed in threshold
		newCap = oldThr;
	else {               // zero initial threshold signifies using defaults
		newCap = DEFAULT_INITIAL_CAPACITY;
		newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
	}
	if (newThr == 0) {
		float ft = (float)newCap * loadFactor;
		newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
				  (int)ft : Integer.MAX_VALUE);
	}
	threshold = newThr;
	@SuppressWarnings({"rawtypes","unchecked"})
		Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
	table = newTab;
	if (oldTab != null) {
		for (int j = 0; j < oldCap; ++j) {
			Node<K,V> e;
			if ((e = oldTab[j]) != null) {
				oldTab[j] = null;
				if (e.next == null)
					newTab[e.hash & (newCap - 1)] = e;
				else if (e instanceof TreeNode)
					((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
				else { // preserve order
					Node<K,V> loHead = null, loTail = null;
					Node<K,V> hiHead = null, hiTail = null;
					Node<K,V> next;
					do {
						next = e.next;
						if ((e.hash & oldCap) == 0) {
							if (loTail == null)
								loHead = e;
							else
								loTail.next = e;
							loTail = e;
						}
						else {
							if (hiTail == null)
								hiHead = e;
							else
								hiTail.next = e;
							hiTail = e;
						}
					} while ((e = next) != null);
					if (loTail != null) {
						loTail.next = null;
						newTab[j] = loHead;
					}
					if (hiTail != null) {
						hiTail.next = null;
						newTab[j + oldCap] = hiHead;
					}
				}
			}
		}
	}
	return newTab;
}

3、總結

  • JDK8對HashMap死循環的解決方法是:擴容後,新數組中的鏈表順序依然與舊數組中的鏈表順序保持一致。
  • JDK8雖然修復了死循環的bug,但是HashMap 還是非線程安全類,仍然會產生數據丟失等問題,線程安全類還是使用ConcurrentHashMap

結束語

本篇至此終於寫完了,關於ConcurrentHashMap的介紹會晚一點寫,下一篇的內容將會分析HashSet

如果本篇內容對你有所幫助,請隨手點個贊,謝謝大家!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章