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

一篇文章了解HashMap和ConcurrentHashMap的扩容机制

HashMap

HashMap 是 Java 中常用的哈希表实现,其扩容机制是保证其高效性能的关键部分。JDK 1.8 对 HashMap 的扩容机制做了较大优化,下面详细解析其扩容过程:

1. 扩容的触发条件

当 HashMap 中的元素数量(size)超过阈值(threshold)时,会触发扩容。

  • 阈值计算公式:threshold = capacity × loadFactor
  • 默认初始容量(capacity)为 16,负载因子(loadFactor)为 0.75
  • 每次扩容时,容量会变为原来的 2 倍(保证容量始终是 2 的幂)

2. 扩容的核心步骤

(1)创建新数组

新建一个容量为原数组 2 倍的数组(newTab)

(2)数据迁移

将原数组(oldTab)中的数据迁移到新数组中,这是扩容的核心操作:

JDK 1.8 采用了更高效的迁移方式:

// 原位置计算
int oldIndex = e.hash & (oldCap - 1);
// 新位置计算(两种可能)
int newIndex = (oldCap & e.hash) == 0 ? oldIndex : oldIndex + oldCap;

这种计算方式的优势:

  • 无需重新计算哈希值
  • 元素要么留在原索引位置,要么迁移到 原索引+旧容量 的位置
  • 避免了 JDK 1.7 中重新哈希带来的性能损耗
(3)处理链表和红黑树
  • 链表迁移:将链表拆分为两个子链表,分别放入新数组的两个可能位置
  • 红黑树迁移
    • 当树节点数小于 6 时,会退化为链表
    • 否则会将红黑树拆分为两个子树,可能是红黑树或链表
(4)更新参数
  • 更新容量为新容量
  • 重新计算阈值(新容量 × 负载因子)
  • 将新数组设置为 HashMap 的 table 属性

3. 扩容的优缺点

优点

  • 采用 2 倍扩容,保证容量始终是 2 的幂,使得哈希计算更高效(位运算)
  • 迁移算法优化,减少了哈希冲突的概率
  • 红黑树的引入避免了极端情况下链表过长导致的性能下降

缺点

  • 扩容过程需要复制所有元素,耗时较长
  • 并发环境下可能导致死循环(JDK 1.7),JDK 1.8 已修复但仍不建议并发使用

4. 扩容机制的注意事项

  • 初始容量选择:如果预知数据量较大,可指定合适的初始容量减少扩容次数
  • 负载因子调整:对迭代性能要求高时可降低负载因子,牺牲空间换时间
  • 线程安全:HashMap 扩容过程中不保证线程安全,多线程环境下建议使用 ConcurrentHashMap

ConcurrentHashMap

ConcurrentHashMap 是 Java 中线程安全的哈希表实现,其扩容机制在保证线程安全的同时,也兼顾了高效性。相比 HashMap,ConcurrentHashMap 的扩容过程更为复杂,下面详细解析其 JDK 1.8 及以上版本的扩容机制:

1. 扩容的触发条件

当 ConcurrentHashMap 满足以下任一条件时,会触发扩容:

  • 元素数量(size)超过阈值(threshold = 容量 × 负载因子)
  • 某个链表长度达到 8 且数组长度小于 64 时,先扩容而非树化

2. 扩容的核心机制

ConcurrentHashMap 采用分段扩容(增量扩容)策略,允许多个线程同时参与扩容,避免了单线程扩容的性能瓶颈。

(1)扩容准备
  1. 计算新容量(原容量的 2 倍)
  2. 创建新数组(nextTable)
  3. 设置扩容标记(sizeCtl = -1 表示正在扩容)
  4. 确定每个线程负责迁移的桶范围
(2)迁移过程(核心步骤)
  1. 分配迁移任务

    • 每个线程通过 CAS 操作认领一段连续的桶进行迁移
    • 用 i 表示当前迁移的桶索引,bound 表示迁移结束的边界
    • 迁移完成后更新 transferIndex 分配新的任务段
  2. 元素迁移
    对每个桶中的元素(链表或红黑树)进行迁移:

    // 计算元素在新数组中的位置
    int newIndex = (node.hash & (newCap - 1));
    // 红黑树迁移
    if (node instanceof TreeBin) {// 拆分红黑树并迁移
    } else {// 链表迁移,保持原有顺序// 分为低位链表和高位链表
    }
    
  3. 迁移完成标记

    • 当所有桶迁移完成后,将 nextTable 设置为新的 table
    • 更新容量和阈值,重置 sizeCtl 为新的阈值

3. 线程协作机制

  • 扩容线程:发现需要扩容时,主动参与迁移工作
  • 读写线程
    • 读操作:如果遇到正在迁移的桶,会读取旧表和新表中的数据
    • 写操作:
      • 对已迁移的桶:直接操作新表
      • 对未迁移的桶:先协助完成迁移,再执行写操作

这种协作机制避免了扩容时的线程阻塞,提高了并发效率。

4. 扩容中的线程安全保障

  1. CAS 操作:用于设置扩容标记、分配迁移任务等
  2. synchronized 锁:迁移时锁定当前桶,防止并发修改
  3. volatile 变量sizeCtlnextTable 等关键变量用 volatile 修饰,保证可见性
  4. 节点标记:迁移中的节点会被标记为 forwardNode,指引线程访问新表

5. 扩容机制的优缺点

优点

  • 支持多线程并发扩容,提高扩容效率
  • 扩容过程中不阻塞读写操作,保证高并发性能
  • 采用增量迁移,避免单线程长时间占用 CPU

缺点

  • 实现复杂,增加了代码维护难度
  • 迁移过程中可能出现短暂的内存占用增加(同时存在新旧两个数组)

两者对比

特性HashMapConcurrentHashMap
线程安全
同步机制桶级 synchronized + CAS
支持 null 键值
扩容方式单线程扩容多线程并发扩容
迭代行为快速失败(fail-fast)弱一致性(不抛异常)
多线程性能差(需额外同步)优(细粒度锁)
http://www.dtcms.com/a/296812.html

相关文章:

  • ESP32入门实战:PC远程控制LED灯完整指南
  • pandas库的数据导入导出,缺失值,重复值处理和数据筛选,matplotlib库 简单图绘制
  • AD一张原理图分成多张原理图
  • iview Select的Option边框显示不全(DatePicker也会出现此类问题)
  • rust-参考与借用
  • 爬虫逆向--Day12--DrissionPage案例分析【小某书评价数据某东评价数据】
  • MySQL零基础教程增删改查实战
  • java后端
  • mujoco playground
  • DBA常用数据库查询语句
  • DevOps 完整实现指南:从理论到实践
  • 论文阅读:《Many-Objective Evolutionary Algorithms: A Survey. 》多目标优化问题的优化目标评估的相关内容介绍
  • Android LiveData 全面解析:原理、使用与最佳实践
  • Rust生态中的LLM实践全解析
  • 【C# 找最大值、最小值和平均值及大于个数和值】2022-9-23
  • 项目质量如何提升?
  • 教育培训系统源码如何赋能企业培训学习?功能设计与私有化部署实战
  • 使用 Vue 实现移动端视频录制与自动截图功能
  • MySQL---索引、事务
  • Docker 打包Vue3项目镜像
  • 互联网广告中的Header Bidding与瀑布流的解析与比较
  • 性能测试-groovy语言1
  • 使用 LLaMA 3 8B 微调一个 Reward Model:从入门到实践
  • 【硬件-笔试面试题】硬件/电子工程师,笔试面试题-19,(知识点:PCB布局布线的设计要点)
  • 类和包的可见性
  • 勾芡 3 步诀:家庭挂汁不翻车
  • Spring Data JPA 中的一个注解NoRepositoryBean
  • Edwards爱德华干泵报警信息表适用于iXH, iXL, iXS, iHand pXH
  • 机器学习的基础知识
  • istio tcp连接超时测试