引言
上一篇咱們分析了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
如果本篇內容對你有所幫助,請隨手點個贊,謝謝大家!