当前位置: 首页 > news >正文

JDK8 HashMap红黑树退化为链表的机制解析

目录

1、数据结构:

2、Fail-Fast机制

2.1、核心作用

2.2、实现原理

2.3、触发场景

2.4、实现细节

2.5、对比

2.6、注意事项

3、核心结论

4、转化安全机制

4.1. 触发场景

4.2. 转换过程

4.3. 并发安全机制

5、设计原因

5.1. 性能权衡

5.2. 空间局部性

5.3. 实际测试数据

6、常见误区

7、实战建议


前言

在之前对jdk8的hashmap的结构进行了深入的学习,可参考:HashMap的扩容机制-CSDN博客


1、数据结构:

jdk8及之后,由hashmap由数组+链表(红黑树组成)。

如下图所示:

        桶数组是用来存储数据元素,链表是用来解决冲突,红黑树是为了提高查询的效率。

数据元素通过映射关系,也就是散列函数,映射到桶数组对应索引的位置。

如下图所示:

如果发生冲突,从冲突的位置拉一个链表,插入冲突的元素。

        如果链表长度>8&数组大小>=64,链表转为红黑树。

        如果红黑树节点个数<6 ,转为链表。


2、Fail-Fast机制

        Fail-Fast(快速失败)是Java集合框架中一种重要的并发修改检测机制,在HashMap中主要用于防止在迭代过程中集合被意外修改而导致数据不一致的问题。

2.1、核心作用

Fail-Fast机制就像集合的"安全警报系统":

  • 实时监控:检测迭代期间的意外修改

  • 快速响应:立即抛出ConcurrentModificationException

  • 预防损害:避免产生不可预知的错误结果

2.2、实现原理

1. 关键变量

// HashMap中的修改计数器
transient int modCount;// 迭代器中保存的计数器快照
int expectedModCount;

2. 工作流程

2.3、触发场景

1. 迭代时修改集合

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);  // 这里会触发fail-fast
}

2. 多线程并发修改

Map<Integer, String> map = new HashMap<>();
map.put(1, "One");new Thread(() -> {map.put(2, "Two");  // 可能触发主线程迭代时fail-fast
}).start();for (Integer key : map.keySet()) {  // 可能抛出异常System.out.println(key);
}

2.4、实现细节

1. 修改计数更新点

// HashMap中的修改操作都会增加modCount
public V put(K key, V value) {// ...++modCount;// ...
}public V remove(Object key) {// ...++modCount;// ...
}public void clear() {// ...++modCount;// ...
}

2. 迭代器检查点

final class KeyIterator extends HashIterator implements Iterator<K> {public final K next() {if (modCount != expectedModCount)throw new ConcurrentModificationException();// ...}
}

2.5、对比

2.6、注意事项

1.正确删除元素

// 错误方式(触发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.性能监控

// 检测异常频率
try {for (Entry<K,V> e : map.entrySet()) {// ...}
} catch (ConcurrentModificationException ex) {metrics.record("fail-fast.triggered");
}

        Fail-Fast机制虽然会给开发者带来一些"麻烦",但它有效地预防了更危险的隐性数据一致性问题,是Java集合框架健壮性的重要保障。

        理解这一机制可以帮助开发者写出更安全的集合操作代码。


3、核心结论

在JDK8+的HashMap中:

  1. 确实存在红黑树退化为链表的机制(当节点数≤6时)

  2. 这不是红黑树自身的特性,而是HashMap的主动优化

  3. 转换是安全的,因为这是在扩容(resize)或删除(remove)时触发的

关于树化与退化阈值如下图所示:


4、转化安全机制

        HashMap在JDK8引入的红黑树转换机制包含严格的安全保障措施,确保在链表与红黑树相互转换时不会破坏数据一致性和线程安全。

4.1. 触发场景

1. 树化(链表 → 红黑树)条件

使用treeifybin()方法。

// HashMap.treeifyBin() 片段
if (binCount >= TREEIFY_THRESHOLD - 1) { // TREEIFY_THRESHOLD=8if (tab.length < MIN_TREEIFY_CAPACITY) // MIN_TREEIFY_CAPACITY=64resize();elsetreeifyBin(tab, hash);
}

双重校验保障

  1. 单链表长度≥8

  2. 哈希表容量≥64

    • 避免小表频繁树化

    • 确保有足够分散的桶空间

2. 退化(红黑树 → 链表)条件

// HashMap.resize() 片段
if (lc <= UNTREEIFY_THRESHOLD)  // UNTREEIFY_THRESHOLD=6tab[index] = loHead.untreeify(map);

安全边界

  • 树节点≤6时才退化(比树化阈值低2,避免频繁转换)

4.2. 转换过程

如下图所示:

1. 链表→红黑树转换流程

关键保障

  • 持有桶头节点锁再进行转换

  • 新建TreeNode时保留原链表顺序(通过next指针)

  • 平衡操作不改变元素哈希位置

2. 红黑树→链表转换流程

如下图所示:

代码示例:

// TreeNode.untreeify() 实现
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); // 新建普通节点if (tl == null)hd = p;elsetl.next = p;tl = p;}return hd;
}

安全保障

  1. 按原有链表顺序(通过TreeNode保留的next指针)重建

  2. 新建普通节点而非修改原节点,避免并发访问问题

  3. 转换完成后原TreeNode可被GC回收

4.3. 并发安全机制

1、转换期间不影响迭代器一致性

abstract class HashIterator {Node<K,V> next;        // 下一个返回的节点Node<K,V> current;     // 当前节点int expectedModCount;  // 修改计数器快照final Node<K,V> nextNode() {if (modCount != expectedModCount)throw new ConcurrentModificationException();// ...}
}

失效保护

  • 迭代期间检测modCount变化

  • 快速失败(fail-fast)机制

2、始终维持元素的原始存储顺序

1. 双向链表维护

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {TreeNode<K,V> parent;  // 红黑树父节点TreeNode<K,V> left;    // 左子树TreeNode<K,V> right;   // 右子树TreeNode<K,V> prev;    // 链表前驱节点(删除时需要)boolean red;// 仍然保留next指针(继承自Entry)
}

双重结构

  • 红黑树结构:parent/left/right

  • 链表结构:next/prev

    • 保证在退化时可以快速重建链表

    • 支持按插入顺序遍历

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);
}

转换过程中始终保持:

  • 键的hashCode不变

  • 键对象的equals()不变

  • 值对象引用不变

3、线程安全(在持有锁的情况下进行)

4、异常处理机制(可进行回滚)

1. 转换失败回滚

try {treeifyBin(tab, hash);
} catch (Throwable t) {tab[index] = originalHead; // 回退到原链表throw t;
}

2. 内存溢出防护

// TreeNode构造时检查内存
if (remaining < treeNodeSpace) {untreeify(); // 立即退化为链表return;
}

        HashMap的转换安全机制通过精细的锁控制、结构隔离和状态校验,在保证性能的同时实现了线程安全和数据一致性。

        这种设计体现了Java集合框架在高并发场景下的工程智慧,也是为什么HashMap能成为最常用的数据结构之一的关键所在。


5、设计原因

5.1. 性能权衡

数学验证

  • 当n=6时:

    • 链表平均查找次数:3次

    • 红黑树查找次数:log₂6≈2.58次

  • 性能差距不大,但红黑树维护成本更高

5.2. 空间局部性

  • 链表节点内存连续访问更友好

  • 红黑树的树节点结构更复杂:、

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {TreeNode<K,V> parent;  // 父节点指针TreeNode<K,V> left;    // 左子树指针TreeNode<K,V> right;   // 右子树指针TreeNode<K,V> prev;    // 前驱节点(仍保留链表结构)boolean red;          // 颜色标记
}

5.3. 实际测试数据

在Java标准库的基准测试中:

  • 节点数=6时,链表比红黑树快约15%

  • 内存占用减少约40%


6、常见误区

1、误区:"红黑树会自动退化为链表"

事实:这是HashMap的主动控制行为。

2、误区:"转换会破坏数据"

事实:元素顺序和内容完全保留。

3、误区:"节点数在7时会频繁转换"

事实:只有在resize/remove时检查阈值。


7、实战建议

  1. 监控树节点比例

// 检查桶的树化情况
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++;
}

优化hashCode()

  • 减少哈希碰撞可避免树化

  • 示例:

// 好的hashCode实现
@Override
public int hashCode() {return Objects.hash(field1, field2, field3); 
}

容量规划

// 预设足够大的initialCapacity
new HashMap<>(expectedSize * 2); 

总结

        JDK的这个设计体现了工程上的精妙权衡:在保持算法理论正确性的同时,针对实际硬件特性和使用场景做出了最优实践选择。

参考文章:

1、HashMap的扩容机制-CSDN博客

相关文章:

  • 为人类文明建一座“永不遗忘”的数字博物馆:Funes 技术解析
  • 【计算机视觉】Car-Plate-Detection-OpenCV-TesseractOCR:车牌检测与识别
  • 在 MyBatis 中实现控制台输出 SQL 参数
  • java学习笔记
  • AI客服问答自动生成文章(基于deepseek实现)
  • ABB电机保护单元通过Profibus DP主站转Modbus TCP网关实现上位机通讯
  • Vulnhub Lazysysadmin靶机攻击实战(一)
  • 硬链接与软连接
  • 如何从极狐GitLab 容器镜像库中删除容器镜像?
  • Bitcoin跨链协议Clementine的技术解析:重构DeFi生态的信任边界
  • 自然语言处理-词性标注的基本概念
  • requests库
  • Edwards爱德华STP泵软件用于操作和监控涡轮分子泵
  • RabbitMQ-运维
  • 单调栈所有模版型题目(1)
  • 使用 NV‑Ingest、Unstructured 和 Elasticsearch 处理非结构化数据
  • vue3 ts 写一个滑动选择的日期选择器组件
  • 值拷贝、浅拷贝和深拷贝
  • 一、每日Github软件分享----QuickGo外链直达工具​
  • LeetCode面试题 17.21 直方图的水量
  • 屈晓华履新四川省社科联党组书记,此前担任省国动办主任
  • 公募基金解读“一揽子金融政策”:增量财政空间或打开,有助于维持A股活力
  • 陈雯出任外交部离退休干部局局长,此前为外交部办公厅副主任
  • 巴方称印军发动24起袭击,巴境内6处地点遭袭致8人死亡
  • 巴基斯坦所有主要城市宣布进入紧急状态,学校和教育机构停课
  • 消费者在天猫一旗舰店换手机电池疑遭套路致手机损坏,平台已介入