Java 常用集合類 HashMap 深度解析
在Java編程中,HashMap 是最常用的數(shù)據(jù)結(jié)構(gòu)之一,它提供了高效的鍵值對存儲和檢索功能。無論是日常開發(fā)還是技術(shù)面試,對 HashMap 的深入理解和熟練應用都是至關(guān)重要的。
HashMap數(shù)據(jù)結(jié)構(gòu)概覽
HashMap是我們比較常用的集合類型,它是以鍵值對的邏輯結(jié)構(gòu)來存儲數(shù)據(jù)的。
- HashMap允許存儲null鍵或者null值的鍵值對。
- HashMap非線程安全。
- HashMap底層初始化用的是數(shù)組+鏈表,當鏈表長度大于8(默認值)時,若size小于64則進行2倍擴容,反之會對對應的數(shù)組桶進行鏈表轉(zhuǎn)紅黑樹操作。
- HashMap默認容量大小為16。
不同版本的HashMap底層數(shù)據(jù)結(jié)構(gòu)
在JDK1.8 之前,底層采用數(shù)組+鏈表,用(n - 1) & hash找到數(shù)組索引位置,若沖突則用拉鏈法解決沖突,當沖突碰撞加劇的時候,可能存在查詢性能退化為O(n)的情況。
JDK1.8 之后底層采用數(shù)組+鏈表作為初始結(jié)構(gòu),當某個桶鏈表長度大于8時,默認情況下,會判斷數(shù)組長度是否小于64,若小于64則使用resize()進行擴容。反之調(diào)用treeifyBin()將bucket內(nèi)所有節(jié)點轉(zhuǎn)紅黑樹節(jié)點:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//......
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//當鏈表元素大于8時,調(diào)用treeifyBin查看當前bucket數(shù)是否大于64,若大于則將bucket節(jié)點轉(zhuǎn)紅黑樹節(jié)點,反之進行bucket擴容
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//......
}
}
//......
}
//......
return null;
}
HashMap是如何解決哈希沖突的?
在JDK7之前HashMap采用的是鏈式尋址法解決哈希沖突的,而JDK8之后則未轉(zhuǎn)紅黑樹前采用的就是鏈式尋址法,轉(zhuǎn)紅黑樹之后就借用紅黑樹天然有序性(黑平衡)解決哈希沖突。
為什么1.8之后要加一個鏈表轉(zhuǎn)紅黑樹的操作
鏈表查詢的時間復雜度為O(n)在數(shù)據(jù)量較少的情況下查詢效率不錯,一旦沖突非常厲害,鏈表數(shù)量暴增的話查詢效率或者添加效率都會十分低下,所以就需要轉(zhuǎn)為紅黑樹,通過黑平衡結(jié)構(gòu)保證插入和查詢效率都為O(logN),并且紅黑樹是黑平衡樹,所以相較于AVL不會進行頻繁的翻轉(zhuǎn)保證平衡的操作。
HashMap底層的數(shù)據(jù)結(jié)構(gòu)紅黑樹算法在大數(shù)據(jù)情況下最大高度可能是多少呢?
理想情況為2log2 (n+1),但是Java中這個情況考慮的因素就很多了:
- 得看看堆區(qū)大小以及這個節(jié)點的大小。
- 就Java而言這種情況很少見,如果大數(shù)據(jù)都在一個bucket中,就說明設(shè)置的哈希算法有問題了。
HashMap幾種構(gòu)造方法
1.默認構(gòu)造函數(shù)的初始化流程
如下所示,僅僅初始化負載因子,默認為0.75f,這個負載因子的作用即當當前數(shù)組大小>數(shù)組容量*負載因子時會進行擴容操作。
public HashMap() {
this.loadFactor = 0.75F;
}
2.將另一個Map存到當前Map中的構(gòu)造函數(shù)工作流程
該方法會將閾值設(shè)置為默認值DEFAULT_LOAD_FACTOR(0.75f),然后將傳入的map通過putMapEntries方法將鍵值對逐個存入。
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
3.指定初始化容量的HashMap
通過外部參數(shù)傳入initialCapacity,初始化map底層數(shù)據(jù)的大小。
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
4.指定容量和負載因子的構(gòu)造函數(shù)
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
HashMap核心源碼詳解
1.HashMap對應put方法工作流程
HashMap的put方法的邏輯比較清晰,大體的邏輯為:
- 通過hash算法得到桶的位置
- 嘗試將鍵值對存到hash計算后的桶的位置中。
- 無沖突直接創(chuàng)建新節(jié)點保存。
- 有沖突則按照鏈表或者紅黑樹的邏輯進行插入。
入口代碼:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
進入putVal,可以看到要做的就是計算出桶的位置然后完成插入。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// table未初始化或者長度為0,進行擴容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//計算(n - 1) & hash并查看是否在桶中,若不在則直接創(chuàng)建一個結(jié)點放到這個桶中
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 桶中已經(jīng)存在元素(處理hash沖突)
else {
Node<K,V> e; K k;
// 判斷table[i]中的元素是否與插入的key一樣,若相同那就直接使用插入的值p替換掉舊的值e。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 判斷插入的是否是紅黑樹節(jié)點
else if (p instanceof TreeNode)
// 放入樹中
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 不是紅黑樹節(jié)點則說明為鏈表結(jié)點
else {
// 不斷遍歷到達鏈表尾部
for (int binCount = 0; ; ++binCount) {
// 不斷往鏈表后面走,若為空則說明到達尾部,直接添加節(jié)點
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 結(jié)點數(shù)量達到閾值(默認為 8 ),執(zhí)行 treeifyBin 方法
// 這個方法會根據(jù) HashMap 數(shù)組來決定是否轉(zhuǎn)換為紅黑樹。
// 只有當數(shù)組長度大于或者等于 64 的情況下,才會執(zhí)行轉(zhuǎn)換紅黑樹操作,以減少搜索時間。否則,就是只是對數(shù)組擴容。
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
// 跳出循環(huán)
break;
}
// 判斷鏈表中結(jié)點的key值與插入的元素的key值是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 相等,跳出循環(huán)
break;
// 用于遍歷桶中的鏈表,與前面的e = p.next組合,可以遍歷鏈表
p = e;
}
}
// 表示在桶中找到key值、hash值與插入元素相等的結(jié)點
if (e != null) {
// 記錄e的value
V oldValue = e.value;
// onlyIfAbsent為false或者舊值為null
if (!onlyIfAbsent || oldValue == null)
//用新值替換舊值
e.value = value;
// 訪問后回調(diào)
afterNodeAccess(e);
// 返回舊值
return oldValue;
}
}
// 結(jié)構(gòu)性修改
++modCount;
// 實際大小大于閾值則擴容
if (++size > threshold)
resize();
// 插入后回調(diào)
afterNodeInsertion(evict);
return null;
}
2.HashMap的get方法的流程
整體邏輯和put也差不多,計算桶的位置,然后看看是那種數(shù)據(jù)結(jié)構(gòu),若是鏈表則遍歷鏈表然后進行hashCode和equals方法比較是否一致然后返回,紅黑樹同理。
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 數(shù)組元素相等
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 桶中不止一個節(jié)點
if ((e = first.next) != null) {
// 若是紅黑樹,則走紅黑樹的遍歷邏輯
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 反之說明這是一個鏈表
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
3.hashMap擴容機制
擴容方法整體做的就是數(shù)組遷移,注釋都在下方,這里我們只需注意JDK核心設(shè)計,就是遷移的核心邏輯代碼如下。 是JDK1.8中的優(yōu)化操作,可以不需要再重新計算每一個元素的哈希值,如下圖所示,將當前元素的hash值&容器舊的容量,如果高位有1則說明他要落到新的bucket中。
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) {
//遍歷,將舊鏈表元素遷移到新的鏈表上
} while ((e = next) != null);
//維護原來的尾節(jié)點
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 維護新擴容的尾節(jié)點
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
4.HashMap 的容量為什么需要是 2 的冪次方,而這個冪為什么是31呢?
先來回答第一個問題,容量為什么是2的冪次方,首先我們步入hashMap的源碼中查看。hashMap計算鍵值對存到桶中索引位置的代碼。
i = (n - 1) & hash
在n為2的次冪情況下,(n - 1) & hash通過數(shù)學公式其實可以推導為 hash%n。
我們假設(shè)hash為1,使用不同的2次冪可以印證我們上面的論述。
1. n為2的2次方: 4===> 3&1==1%4
2. n為2的3次方: 8===> 7&1==1%8
3. .....
除此之外,使用2的次冪進行計算時碰撞次數(shù)會少于非2的次冪。同樣的,我們假設(shè)hash值的7、8、9、10。hashMap的容量n分別假設(shè)為8(2的3次方)和9。
n為16的計算結(jié)果如下,碰撞0次。
7===>7
8===>0
9===>1
10===>2
n為9的計算結(jié)果,碰撞2次。
7===>0
8===>8
9===>8
10===>8
再來了解一下hashCode的東西可以看到計算機hash的乘積數(shù)寫死為31,這是為什么呢?
int hash = 0;
private char[] value;
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
我們再來看看StackOverflow的回答:
The value 31 was chosen because it is an odd prime. If it were even and the multiplication overflowed, information would be lost, asmultiplication by 2 is equivalent to shifting. The advantage of usinga prime is less clear, but it is traditional. A nice property of 31 isthat the multiplication can be replaced by a shift and a subtractionfor better performance: 31 * i == (i << 5) - i. Modern VMs do thissort of optimization automatically.
大意說的是如果使用雙數(shù)的話,計算就是使用<<1,這樣的計算很可能會出現(xiàn)數(shù)據(jù)溢出,使用奇數(shù)31則會JVM會將其優(yōu)化成一個數(shù)學公式:31 * i == (i << 5) - i,如此依賴無論怎么計算hash值都不會超過int的最大值2^31-1 (0111 1111 | 1111 1111 | 1111 1111 | 1111 1111) ,那么問題又來了,別的小于31的奇數(shù)不會超過int的范圍,為什么乘積數(shù)不用別的值而一定要用31呢?我們不妨寫一個demo進行實驗一下,不同的乘積數(shù)計算出的hash的值的碰撞數(shù)是多少
基于源碼推導出hashCode優(yōu)化后的公式,31 * i == (i << 5) - i 推導過程就在下方:
private static int hash;
/**
* 31 * h
* ===> (2^5-1) * h
* ====> (1<< 5-1 ) * h
* ===> (1<< 5) * h -h
* 最終結(jié)果
* ====> h << 5 - h
* 從而避免計算溢出 以及使用位移提升性能
*/
public int hashCode(char[] value, int num) {
hash = resetHash();
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = num * h + val[i];
}
hash = h;
}
return h;
}
private int resetHash() {
return 0;
}
而且乘積數(shù)為為是31還有下面兩個好處:
- 沖突最少。
- 31計算的值都在取值范圍內(nèi)。
對此,筆者使用了下面這段代碼印證這個結(jié)果:
public int hashCode(char[] value, int num) {
hash = resetHash();
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
/**
* 31 * h
* ===> (2^5-1) * h
* ====> (1<< 5-1 ) * h
* ===> (1<< 5) * h -h
* 最終結(jié)果
* ====> h << 5 - h
* 從而避免計算溢出 以及使用位移提升性能
*/
h = num * h + val[i];
}
hash = h;
}
return h;
}
private int resetHash() {
return 0;
}
@Test
public void hashCodeTest() {
int size = 1000_0000;
caculHashCode(size,2);
caculHashCode(size,4);
caculHashCode(size,7);
caculHashCode(size,31);
caculHashCode(size,32);
caculHashCode(size,33);
caculHashCode(size,64);
/**
* 輸出結(jié)果 31碰撞率低 31之后的質(zhì)數(shù)也行 但是最大值超過int 范圍了
* 2:重復了9997896
* 4:重復了9939942
* 7:重復了8696061
* 31:重復了0
* 32:重復了5900000
* 33:重復了8
* 64:重復了9590000
*/
}
private void caculHashCode(int size, int num) {
HashSet<Integer> set2 = new HashSet<>();
for (int i = 0; i < size; i++) {
set2.add(hashCode(String.valueOf(i).toCharArray(), num));
}
System.out.println(num + ":重復了" + (size - set2.size()));
}
總結(jié)一下:
- 通過2的次冪使得公式,可以使得公式變?yōu)槿∧_\算,提升計算效率。
- 次冪為31計算結(jié)果永遠小于int類型避免計算溢出,在int類型區(qū)間中31次冪碰撞率最低。
5.重寫equals為什么要重寫hashcode?
我們在日常開發(fā)中,某些情況下我們判斷對象是否相等需要有自己的一套邏輯,這時候我們就需要重寫equals方法,但我們在重寫equals方法時不重寫hashCode方法,很可能會造成很嚴重的集合操作事故。
我們以以這樣的一個場景為例,由于業(yè)務的需要,我們判斷產(chǎn)品對象的邏輯需要重寫,只有id相等的產(chǎn)品對象才是相等的對象。所以我們重寫了產(chǎn)品對象的equals方法,關(guān)鍵代碼如下所示:
public class Product {
private Integer id;
private String name;
public Product(Integer id, String name) {
this.id = id;
this.name = name;
}
.....get、set
@Override
public String toString() {
return "Product{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
// 重寫equals
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Product product = (Product) o;
return Objects.equals(id, product.id);
}
這時候有個場景要求我們對產(chǎn)品進行去重操作,代碼以及運行結(jié)果如下所示,可以看到明明是兩個我們邏輯上相同的產(chǎn)品卻都被存到set集合中,這是為什么呢?我們不妨看看set的add源碼
public static void main(String[] args) {
Product product1 = new Product(1, "id為1的饅頭舊版本");
Product product2 = new Product(1, "id為1的饅頭新版本");
HashSet<Product> products = new HashSet<Product>();
boolean contains = products.contains(product1);
products.add(product1);
products.add(product2);
// 使用equals判斷是否相等
System.out.println(product1.equals(product2));
// 查看HashSet中元素個數(shù)
System.out.println(products.size());
for (Product product : products) {
System.out.println(product.toString());
}
/**
* true
* 2
* Product{id=1, name='id為1的饅頭舊版本'}
* Product{id=1, name='id為1的饅頭新版本'}
*/
}
首先來到add的淺層調(diào)用,我們不妨直接步入查看:
public boolean add(E e) {
//調(diào)用map方法完成元素插入
return map.put(e, PRESENT)==null;
}
可以看到put代碼本質(zhì)上就算通過key得到一個hash值,這個值和上一次插入的元素hash值一樣,然后調(diào)用putVal嘗試將元素插入:
public V put(K key, V value) {
//通過hash計算得到hash值,然后調(diào)用putVal進行元素插入
return putVal(hash(key), key, value, false, true);
}
假設(shè)我們沒有重寫equals就會發(fā)現(xiàn),因為上一步hash值一樣,但是equals沒有重寫導致兩個元素比較的是引用地址,因為引用地址的不同導致hashMap認定兩者不為同一個key進而導致邏輯上認為相同的元素,卻都插入到map中:
對此我們給出putVal的源碼,讀者可基于該說法進行代入性的調(diào)試:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//......
else {
Node<K,V> e; K k;
//如果hash相同且對象相等則走這段邏輯,設(shè)置一個值直接返回不進行插入操作
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//否則進行插入操作
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
.......
}
6.HashMap 常見遍歷以及安全刪除代碼要怎么做?
示例代碼如下,讀者可自行調(diào)試,大抵是建議使用entrySet,以及在循環(huán)時安全刪除建議使用entrySet的迭代器形式:
private static HashMap<String, String> map = new HashMap();
@Before
public void before() {
int size = 1000_0000;
for (int i = 0; i < size; i++) {
map.put(String.valueOf(i), String.valueOf(i));
}
}
@Test
public void CycleTest() {
long start = System.currentTimeMillis();
Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, String> entry = iterator.next();
String key = entry.getKey();
String value = entry.getValue();
}
long end = System.currentTimeMillis();
System.out.println("entry iterator遍歷:" + (end - start));
start = System.currentTimeMillis();
Iterator<String> keySetIterator = map.keySet().iterator();
while (keySetIterator.hasNext()) {
String key = keySetIterator.next();
String value = map.get(key);
}
end = System.currentTimeMillis();
System.out.println("keySet Iterator遍歷:" + (end - start));
start = System.currentTimeMillis();
for (Map.Entry<String, String> entry : map.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
}
end = System.currentTimeMillis();
System.out.println("entrySet 遍歷:" + (end - start));
start = System.currentTimeMillis();
for (String key : map.keySet()) {
String resultKey = key;
String value = map.get(key);
}
end = System.currentTimeMillis();
System.out.println("foreach keyset 遍歷:" + (end - start));
start = System.currentTimeMillis();
map.forEach((k, v) -> {
String key = k;
String value = v;
});
end = System.currentTimeMillis();
System.out.println("lambda 遍歷:" + (end - start));
start = System.currentTimeMillis();
map.entrySet().stream().forEach((entry)->{
String key=entry.getKey();
String value=entry.getValue();
});
end = System.currentTimeMillis();
System.out.println("stream 遍歷:" + (end - start));
start = System.currentTimeMillis();
map.entrySet().parallelStream().forEach((entry)->{
String key=entry.getKey();
String value=entry.getValue();
});
end = System.currentTimeMillis();
System.out.println("并行流 遍歷:" + (end - start));
/**
* 輸出結(jié)果 entrySet性能最好
* entry iterator遍歷:228
* keySet Iterator遍歷:284
* entrySet 遍歷:228
* foreach keyset 遍歷:284
* lambda 遍歷:237
* stream 遍歷:230
* 并行流 遍歷:134
*/
/**
* 兩個entry反編譯的字節(jié)碼一樣說明時長一樣
* long start = System.currentTimeMillis();
*
* Entry entry;
* String var6;
* for(Iterator iterator = map.entrySet().iterator(); iterator.hasNext(); var6 = (String)entry.getValue()) {
* entry = (Entry)iterator.next();
* String key = (String)entry.getKey();
* }
*
* long end = System.currentTimeMillis();
* System.out.println("entry iterator遍歷:" + (end - start));
*
*
* start = System.currentTimeMillis();
*
* String var10;
* Iterator var13;
* Entry entry;
* for(var13 = map.entrySet().iterator(); var13.hasNext(); var10 = (String)entry.getValue()) {
* entry = (Entry)var13.next();
* String key = (String)entry.getKey();
* }
*
* end = System.currentTimeMillis();
* System.out.println("entrySet 遍歷:" + (end - start));
*/
/**
* 安全刪除
*/
Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, String> entry = it.next();
if (entry.getKey() .equals("1") ) {
// 刪除
System.out.println("del:" + entry.getKey());
iterator.remove();
} else {
System.out.println("show:" + entry.getKey());
}
}
}
HashMap多線程可能導致的問題
具體可以參考筆者這篇文章,大致原因是JDK7版本的HashMap在多線程擴容期間,一個線程指向遷移節(jié)點后被掛起,另一個線程完成擴容后。這個線程重新那會CPU執(zhí)行權(quán)在執(zhí)行原有的遷移邏輯,會造成死循環(huán)進而打爆CPU 100%問題,而JDK8則可能會導致同一個兩個key計算到相同的hash值進而導致后put的元素將前一個元素覆蓋。