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

news2025/5/10 17:25:53

目录

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=8
    if (tab.length < MIN_TREEIFY_CAPACITY) // MIN_TREEIFY_CAPACITY=64
        resize();
    else
        treeifyBin(tab, hash);
}

双重校验保障

  1. 单链表长度≥8

  2. 哈希表容量≥64

    • 避免小表频繁树化

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

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

// HashMap.resize() 片段
if (lc <= UNTREEIFY_THRESHOLD)  // UNTREEIFY_THRESHOLD=6
    tab[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;
        else
            tl.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博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2372482.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【基础】模型上下文协议(Model Context Protocol, MCP)根本原理与工作机制详解

一、MCP的根本原理 模型上下文协议&#xff08;MCP&#xff09;是一种标准化接口协议&#xff0c;旨在解决AI系统&#xff08;尤其是大型语言模型&#xff0c;LLM&#xff09;与外部工具、数据源之间的交互碎片化问题。其核心原理可以概括为以下三点&#xff1a; 统一接口抽象…

霸王茶姬微信小程序自动化签到系统完整实现解析

霸王茶姬微信小程序自动化签到系统完整实现解析 技术栈&#xff1a;Node.js 微信小程序API MD5动态签名 一、脚本全景架构 功能模块图 #mermaid-svg-0vx5W2xo0IZWn6mH {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-s…

p2p虚拟服务器

ZeroTier Central ✅ 推荐工具&#xff1a;ZeroTier&#xff08;免费、稳定、跨平台&#xff09; ZeroTier 可以帮你把多台设备&#xff08;无论是否跨网&#xff09;加入一个虚拟局域网&#xff0c;彼此间可以像在同一个 LAN 中通信&#xff0c;UDP 视频、文件传输、SSH 等都…

python实现点餐系统

使用python实现点餐系统的增加菜品及价格&#xff0c;删除菜品&#xff0c;查询菜单&#xff0c;点菜以及会员折扣价等功能。 代码&#xff1a; 下面展示一些 内联代码片。 # coding utf-8menu {拍黄瓜: 6, 小炒肉: 28, 西红柿炒蛋: 18, 烤鱼: 30, 红烧肉: 38, 手撕鸡: 45,…

(三)毛子整洁架构(Infrastructure层/DapperHelper/乐观锁)

文章目录 项目地址一、Infrastructure Layer1.1 创建Application层需要的服务1. Clock服务2. Email 服务3. 注册服务 1.2 数据库服务1. 表配置Configurations2. Respository实现3. 数据库链接Factory实现4. Dapper的DataOnly服务实现5. 所有数据库服务注册 1.3 基于RowVersion的…

探索Stream流:高效数据处理的秘密武器

不可变集合 stream流 Stream流的使用步骤&#xff1a; 先得到一条Stream流&#xff08;流水线&#xff09;&#xff0c;并把数据放上去 使用中间方法对流水线上的数据进行操作 使用终结方法对流水线上的数据进行操作 Stream流的中间方法 注意1&#xff1a;中间方法&#xff0…

虚拟机ubantu20.04系统桥接模式下无法ping通外网,但可以ping通本机的解决方案

1.出现的问题&#xff1a; 虚拟机ubantu20.04系统桥接模式下无法ping通外网,但可以ping通本机。 2.解决方案&#xff1a; 如果 DHCP 未分配 IP 地址&#xff0c;可以手动配置静态 IP&#xff1a; 1.编辑网络配置文件&#xff1a; sudo nano /etc/netplan/01-netcfg.yaml 2…

日常知识点之随手问题整理(思考单播,组播,广播哪个更省带宽)

新入职的公司在某些场景下无脑使用组播技术&#xff0c;自己突然就意识到一个问题&#xff1a;单播&#xff0c;组播&#xff0c;广播&#xff0c;哪个更省带宽&#xff1f; 有所收获&#xff0c;做点笔记&#xff0c;仅仅是个人理解~ 1&#xff1a;简单理解 单播&#xff1…

qtcreater配置opencv

我配置opencv不管是按照网上的教程还是deep seek发现都有些问题&#xff0c;下面是我的配置方法以及实践成功的心得 电脑环境 windows平台qt6 下载 我这里直接提供官网下载地址&#xff1a;https://opencv.org/releases/ 我下载的是最新版&#xff0c;下载后是一个.exe文件…

机器学习-数据集划分和特征工程

一.数据集划分 API函数&#xff1a; sklearn.model_selection.train_test_split(*arrays&#xff0c;**options) 参数&#xff1a; - arrays&#xff1a;多个数组&#xff0c;可以是列表&#xff0c;numpy数组&#xff0c;也可以是dataframe数据框等 - options&#xff1a;&…

MySQL C API高效编程:C语言实现数据库操作的深入解析

知识点【MySQL C API】 1、头文件及MYSQL * 句柄 //头文件 #include <mysql/mysql.h>1、MYSQL MYSQL是一个结构体&#xff0c;封装了与数据库连接相关的所有状态&#xff0c;配置和数据。 2、MYSQL *的本质 类似于 FILE*&#xff0c;代表一个与数据库连接的通道&…

MySQL初阶:数据库约束和表的设计

数据库约束 数据库约束是针对数据库中的表中的数据进行施加规则和条件&#xff0c;用于确保数据的准确性和可靠性。 数据库约束类型 1&#xff09;not null 非空类型 &#xff1a;指定非空类型的列不能存储null&#xff0c;如果插入的数据是null便会报错。 2&#xff09;de…

LeetCode 解题思路 47(最长回文子串、最长公共子序列)

解题思路&#xff1a; dp 数组的含义&#xff1a; dp[i][j] 是否为回文子串。递推公式&#xff1a; dp[i][j] s.charAt(i) s.charAt(j) && dp[i 1][j - 1]。dp 数组初始化&#xff1a; 单字符 dp[i][i] true&#xff0c;双字符 dp[i][i 1] s.charAt(i) s.charA…

嵌入式培训之C语言学习完(十七)结构体、共用体、枚举、typedef关键字与位运算

目录 一、结构体&#xff08;struct关键字&#xff09; &#xff08;一&#xff09;声明一个结构体数据类型 &#xff08;二&#xff09;结构体的成员初始化与赋值 a、结构体变量赋值 b、结构体成员初始化 c、结构体的定义形式 &#xff08;三&#xff09;考点&#xff…

《软件项目经济性论证报告模板:全面解析与策略建议》

《软件项目经济性论证报告模板:全面解析与策略建议》 一、引言 1.1 项目背景阐述 在数字化浪潮席卷全球的当下,各行业对软件的依赖程度日益加深。[行业名称] 行业也不例外,随着业务规模的不断扩张、业务复杂度的持续提升以及市场竞争的愈发激烈,对高效、智能、定制化软件…

关税冲击下,FBA国际物流企业如何靠智能拓客跑出增长“加速度”?

国际物流行业正迎来前所未有的增长机遇。据中研普华最新报告&#xff0c;2025年全球物流市场规模已突破6.27万亿美元&#xff0c;其中中国跨境物流市场预计达2.71万亿元。在全球化与数字化双轮驱动下&#xff0c;国际物流从“规模扩张”迈向“价值重构”。可以说&#xff0c;国…

vue源代码采用的设计模式分解

No.大剑师精品GIS教程推荐0地图渲染基础- 【WebGL 教程】 - 【Canvas 教程】 - 【SVG 教程】 1Openlayers 【入门教程】 - 【源代码示例 300】 2Leaflet 【入门教程】 - 【源代码图文示例 150】 3MapboxGL【入门教程】 - 【源代码图文示例150】 4Cesium 【入门教程】…

使用 JavaScript 实现数据导出为 Excel 和 CSV 文件

在 Web 开发中&#xff0c;经常会遇到需要将数据导出为文件的需求&#xff0c;例如将数据导出为 Excel 或 CSV 文件。今天&#xff0c;我们就来探讨如何使用 JavaScript 实现这一功能。 一、实现思路 我们通过 HTML 创建一个按钮&#xff0c;点击按钮时&#xff0c;触发 Java…

eNSP中路由器RIP协议配置完整实验实验和命令解释

一、实验拓扑 二、配置命令 R1配置并先测试一下连通性 R1、R2和R3接口配置完后再测试连通性&#xff0c;直连路由可通 启动RIP进程&#xff0c;宣告直连网络 查看路由表&#xff0c;测试连通性 环回接口配置 三、命令解释及注意事项 配置命令逐行解释 system-view: 从用户视…

密码学--AES

一、实验目的 1、完成AES算法中1轮加密和解密操作 2、掌握AES的4个基本处理步骤 3、理解对称加密算法的“对称”思想 二、实验内容 1、题目内容描述 &#xff08;1&#xff09;利用C语言实现字节代换和逆向字节代换&#xff0c;字节查S盒代换 &#xff08;2&#xff09;利…