JDK8 HashMap紅黑樹退化為鏈表的機(jī)制方式
1、數(shù)據(jù)結(jié)構(gòu)
jdk8及之后,由hashmap由數(shù)組+鏈表(紅黑樹組成)。
如下圖所示:
桶數(shù)組是用來存儲數(shù)據(jù)元素,鏈表是用來解決沖突,紅黑樹是為了提高查詢的效率。
數(shù)據(jù)元素通過映射關(guān)系,也就是散列函數(shù),映射到桶數(shù)組對應(yīng)索引的位置。
如下圖所示:
如果發(fā)生沖突,從沖突的位置拉一個(gè)鏈表,插入沖突的元素。
如果鏈表長度>8&數(shù)組大小>=64,鏈表轉(zhuǎn)為紅黑樹。
如果紅黑樹節(jié)點(diǎn)個(gè)數(shù)<6 ,轉(zhuǎn)為鏈表。
2、Fail-Fast機(jī)制
Fail-Fast(快速失?。┦荍ava集合框架中一種重要的并發(fā)修改檢測機(jī)制,在HashMap中主要用于防止在迭代過程中集合被意外修改而導(dǎo)致數(shù)據(jù)不一致的問題。
2.1、核心作用
Fail-Fast機(jī)制就像集合的"安全警報(bào)系統(tǒng)":
- 實(shí)時(shí)監(jiān)控:檢測迭代期間的意外修改
- 快速響應(yīng):立即拋出
ConcurrentModificationException
- 預(yù)防損害:避免產(chǎn)生不可預(yù)知的錯(cuò)誤結(jié)果
2.2、實(shí)現(xiàn)原理
1. 關(guān)鍵變量
// HashMap中的修改計(jì)數(shù)器 transient int modCount; // 迭代器中保存的計(jì)數(shù)器快照 int expectedModCount;
2. 工作流程
2.3、觸發(fā)場景
1. 迭代時(shí)修改集合
Map<String, Integer> map = new HashMap<>(); map.put("A", 1); map.put("B", 2); Iterator<String> it = map.keySet().iterator(); while (it.hasNext()) { String key = it.next(); map.put("C", 3); // 這里會觸發(fā)fail-fast }
2. 多線程并發(fā)修改
Map<Integer, String> map = new HashMap<>(); map.put(1, "One"); new Thread(() -> { map.put(2, "Two"); // 可能觸發(fā)主線程迭代時(shí)fail-fast }).start(); for (Integer key : map.keySet()) { // 可能拋出異常 System.out.println(key); }
2.4、實(shí)現(xiàn)細(xì)節(jié)
1. 修改計(jì)數(shù)更新點(diǎn)
// HashMap中的修改操作都會增加modCount public V put(K key, V value) { // ... ++modCount; // ... } public V remove(Object key) { // ... ++modCount; // ... } public void clear() { // ... ++modCount; // ... }
2. 迭代器檢查點(diǎn)
final class KeyIterator extends HashIterator implements Iterator<K> { public final K next() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); // ... } }
2.5、對比
2.6、注意事項(xiàng)
1.正確刪除元素:
// 錯(cuò)誤方式(觸發(fā)fail-fast) for (String key : map.keySet()) { if (key.equals("remove")) { map.remove(key); // 直接修改原集合 } } // 正確方式(使用迭代器的remove) Iterator<String> it = map.keySet().iterator(); while (it.hasNext()) { if (it.next().equals("remove")) { it.remove(); // 不會增加modCount } }
2.多線程解決方案:
- 使用
ConcurrentHashMap
替代 - 或者使用顯式同步:
synchronized(map) { for (String key : map.keySet()) { // 操作代碼 } }
3.性能監(jiān)控:
// 檢測異常頻率 try { for (Entry<K,V> e : map.entrySet()) { // ... } } catch (ConcurrentModificationException ex) { metrics.record("fail-fast.triggered"); }
Fail-Fast機(jī)制雖然會給開發(fā)者帶來一些"麻煩",但它有效地預(yù)防了更危險(xiǎn)的隱性數(shù)據(jù)一致性問題,是Java集合框架健壯性的重要保障。
理解這一機(jī)制可以幫助開發(fā)者寫出更安全的集合操作代碼。
3、核心結(jié)論
在JDK8+的HashMap中:
- 確實(shí)存在紅黑樹退化為鏈表的機(jī)制(當(dāng)節(jié)點(diǎn)數(shù)≤6時(shí))
- 這不是紅黑樹自身的特性,而是HashMap的主動優(yōu)化
- 轉(zhuǎn)換是安全的,因?yàn)檫@是在擴(kuò)容(resize)或刪除(remove)時(shí)觸發(fā)的
關(guān)于樹化與退化閾值如下圖所示:
4、轉(zhuǎn)化安全機(jī)制
HashMap在JDK8引入的紅黑樹轉(zhuǎn)換機(jī)制包含嚴(yán)格的安全保障措施,確保在鏈表與紅黑樹相互轉(zhuǎn)換時(shí)不會破壞數(shù)據(jù)一致性和線程安全。
4.1. 觸發(fā)場景
1. 樹化(鏈表 → 紅黑樹)條件
使用treeifybin()方法。
// HashMap.treeifyBin() 片段 if (binCount >= TREEIFY_THRESHOLD - 1) { // TREEIFY_THRESHOLD=8 if (tab.length < MIN_TREEIFY_CAPACITY) // MIN_TREEIFY_CAPACITY=64 resize(); else treeifyBin(tab, hash); }
雙重校驗(yàn)保障:
單鏈表長度≥8
哈希表容量≥64
- 避免小表頻繁樹化
- 確保有足夠分散的桶空間
2. 退化(紅黑樹 → 鏈表)條件
// HashMap.resize() 片段 if (lc <= UNTREEIFY_THRESHOLD) // UNTREEIFY_THRESHOLD=6 tab[index] = loHead.untreeify(map);
安全邊界:
- 樹節(jié)點(diǎn)≤6時(shí)才退化(比樹化閾值低2,避免頻繁轉(zhuǎn)換)
4.2. 轉(zhuǎn)換過程
如下圖所示:
1. 鏈表→紅黑樹轉(zhuǎn)換流程
關(guān)鍵保障:
- 持有桶頭節(jié)點(diǎn)鎖再進(jìn)行轉(zhuǎn)換
- 新建TreeNode時(shí)保留原鏈表順序(通過next指針)
- 平衡操作不改變元素哈希位置
2. 紅黑樹→鏈表轉(zhuǎn)換流程
如下圖所示:
代碼示例:
// TreeNode.untreeify() 實(shí)現(xiàn) final Node<K,V> untreeify(HashMap<K,V> map) { Node<K,V> hd = null, tl = null; for (TreeNode<K,V> q = this; q != null; q = q.next) { Node<K,V> p = map.replacementNode(q, null); // 新建普通節(jié)點(diǎn) if (tl == null) hd = p; else tl.next = p; tl = p; } return hd; }
安全保障:
- 按原有鏈表順序(通過TreeNode保留的next指針)重建
- 新建普通節(jié)點(diǎn)而非修改原節(jié)點(diǎn),避免并發(fā)訪問問題
- 轉(zhuǎn)換完成后原TreeNode可被GC回收
4.3. 并發(fā)安全機(jī)制
1、轉(zhuǎn)換期間不影響迭代器一致性
abstract class HashIterator { Node<K,V> next; // 下一個(gè)返回的節(jié)點(diǎn) Node<K,V> current; // 當(dāng)前節(jié)點(diǎn) int expectedModCount; // 修改計(jì)數(shù)器快照 final Node<K,V> nextNode() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); // ... } }
失效保護(hù):
- 迭代期間檢測modCount變化
- 快速失敗(fail-fast)機(jī)制
2、始終維持元素的原始存儲順序
1. 雙向鏈表維護(hù)
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { TreeNode<K,V> parent; // 紅黑樹父節(jié)點(diǎn) TreeNode<K,V> left; // 左子樹 TreeNode<K,V> right; // 右子樹 TreeNode<K,V> prev; // 鏈表前驅(qū)節(jié)點(diǎn)(刪除時(shí)需要) boolean red; // 仍然保留next指針(繼承自Entry) }
雙重結(jié)構(gòu):
紅黑樹結(jié)構(gòu):parent/left/right
鏈表結(jié)構(gòu):next/prev
- 保證在退化時(shí)可以快速重建鏈表
- 支持按插入順序遍歷
2. 哈希值不變性
// TreeNode既保持hash值又維持鏈表順序 Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) { return new Node<>(p.hash, p.key, p.value, next); }
轉(zhuǎn)換過程中始終保持:
- 鍵的hashCode不變
- 鍵對象的equals()不變
- 值對象引用不變
3、線程安全(在持有鎖的情況下進(jìn)行)
4、異常處理機(jī)制(可進(jìn)行回滾)
1. 轉(zhuǎn)換失敗回滾
try { treeifyBin(tab, hash); } catch (Throwable t) { tab[index] = originalHead; // 回退到原鏈表 throw t; }
2. 內(nèi)存溢出防護(hù)
// TreeNode構(gòu)造時(shí)檢查內(nèi)存 if (remaining < treeNodeSpace) { untreeify(); // 立即退化為鏈表 return; }
HashMap的轉(zhuǎn)換安全機(jī)制通過精細(xì)的鎖控制、結(jié)構(gòu)隔離和狀態(tài)校驗(yàn),在保證性能的同時(shí)實(shí)現(xiàn)了線程安全和數(shù)據(jù)一致性。
這種設(shè)計(jì)體現(xiàn)了Java集合框架在高并發(fā)場景下的工程智慧,也是為什么HashMap能成為最常用的數(shù)據(jù)結(jié)構(gòu)之一的關(guān)鍵所在。
5、設(shè)計(jì)原因
5.1. 性能權(quán)衡
數(shù)學(xué)驗(yàn)證:
當(dāng)n=6時(shí):
- 鏈表平均查找次數(shù):3次
- 紅黑樹查找次數(shù):log?6≈2.58次
- 性能差距不大,但紅黑樹維護(hù)成本更高
5.2. 空間局部性
- 鏈表節(jié)點(diǎn)內(nèi)存連續(xù)訪問更友好
- 紅黑樹的樹節(jié)點(diǎn)結(jié)構(gòu)更復(fù)雜:
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { TreeNode<K,V> parent; // 父節(jié)點(diǎn)指針 TreeNode<K,V> left; // 左子樹指針 TreeNode<K,V> right; // 右子樹指針 TreeNode<K,V> prev; // 前驅(qū)節(jié)點(diǎn)(仍保留鏈表結(jié)構(gòu)) boolean red; // 顏色標(biāo)記 }
5.3. 實(shí)際測試數(shù)據(jù)
在Java標(biāo)準(zhǔn)庫的基準(zhǔn)測試中:
- 節(jié)點(diǎn)數(shù)=6時(shí),鏈表比紅黑樹快約15%
- 內(nèi)存占用減少約40%
6、常見誤區(qū)
1、誤區(qū):"紅黑樹會自動退化為鏈表"
事實(shí):這是HashMap的主動控制行為。
2、誤區(qū):"轉(zhuǎn)換會破壞數(shù)據(jù)"
事實(shí):元素順序和內(nèi)容完全保留。
3、誤區(qū):"節(jié)點(diǎn)數(shù)在7時(shí)會頻繁轉(zhuǎn)換"
事實(shí):只有在resize/remove時(shí)檢查閾值。
7、實(shí)戰(zhàn)建議
監(jiān)控樹節(jié)點(diǎn)比例
// 檢查桶的樹化情況 Field tableField = HashMap.class.getDeclaredField("table"); tableField.setAccessible(true); Node<?,?>[] table = (Node<?,?>[]) tableField.get(map); int trees = 0; for (Node<?,?> node : table) { if (node instanceof TreeNode) trees++; }
優(yōu)化hashCode()
- 減少哈希碰撞可避免樹化
- 示例:
// 好的hashCode實(shí)現(xiàn) @Override public int hashCode() { return Objects.hash(field1, field2, field3); }
容量規(guī)劃
// 預(yù)設(shè)足夠大的initialCapacity new HashMap<>(expectedSize * 2);
總結(jié)
JDK的這個(gè)設(shè)計(jì)體現(xiàn)了工程上的精妙權(quán)衡:在保持算法理論正確性的同時(shí),針對實(shí)際硬件特性和使用場景做出了最優(yōu)實(shí)踐選擇。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Java線程實(shí)現(xiàn)時(shí)間動態(tài)顯示
這篇文章主要為大家詳細(xì)介紹了Java線程實(shí)現(xiàn)時(shí)間動態(tài)顯示,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-04-04idea ssm項(xiàng)目java程序使用十六進(jìn)制rxtx包向串口發(fā)送指令的方法
這篇文章主要介紹了idea ssm項(xiàng)目java程序向串口發(fā)送指令并且使用十六進(jìn)制 rxtx包,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-08-08一不小心就讓Java開發(fā)踩坑的fail-fast是個(gè)什么鬼?(推薦)
這篇文章主要介紹了Java fail-fast,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04Spring?Bean注冊與注入實(shí)現(xiàn)方法詳解
首先,要學(xué)習(xí)Spring中的Bean的注入方式,就要先了解什么是依賴注入。依賴注入是指:讓調(diào)用類對某一接口的實(shí)現(xiàn)類的實(shí)現(xiàn)類的依賴關(guān)系由第三方注入,以此來消除調(diào)用類對某一接口實(shí)現(xiàn)類的依賴。Spring容器中支持的依賴注入方式主要有屬性注入、構(gòu)造函數(shù)注入、工廠方法注入2022-10-10Springboot實(shí)現(xiàn)推薦系統(tǒng)的協(xié)同過濾算法
協(xié)同過濾算法是一種在推薦系統(tǒng)中廣泛使用的算法,用于預(yù)測用戶對物品(如商品、電影、音樂等)的偏好,從而實(shí)現(xiàn)個(gè)性化推薦,下面給大家介紹Springboot實(shí)現(xiàn)推薦系統(tǒng)的協(xié)同過濾算法,感興趣的朋友一起看看吧2025-05-05