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

一篇文章了解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.xdnf.cn/news/16192.html

相关文章:

  • Node.js 中的内置模板path
  • 论文阅读:《Many-Objective Evolutionary Algorithms: A Survey. 》多目标优化问题的优化目标评估的相关内容介绍
  • 机器翻译编程
  • 【安卓笔记】解决livedata粘性事件
  • 在 Alpine Linux 中创建虚拟机时 Cgroup 挂在失败的现象
  • Springboot宠物用品商城的设计与实现
  • 详解力扣高频SQL50题之197. 上升的温度【简单】
  • 星慈光编程虫2号小车讲解第二篇--向左向右平移
  • Python编程进阶知识之第五课处理数据(matplotlib)
  • Unity VS Unreal Engine ,“电影像游戏的时代” 新手如何抉择引擎?(结)
  • 100条SQL语句分类精讲:从基础到进阶的实操指南
  • 医疗系统国产化实录:SQL Server国产替代,乙方保命指南
  • 机器学习的基础知识
  • 洛谷 P1996 约瑟夫问题之题解
  • kafka的shell操作
  • 多源信息融合智能投资【“图神经网络+强化学习“的融合架构】【低配显卡正常运行】
  • MapStruct类型转换接口未自动注入到spring容器中
  • 快速将前端得依赖打为tar包(yarn.lock版本)并且推送至nexus私有依赖仓库(笔记)
  • 《C++》面向对象编程--类(下)
  • LLM中的位置嵌入矩阵(Position Embedding Matrix)是什么
  • matrix-breakout-2-morpheus靶机通关教程
  • DBA常用数据库查询语句
  • Python爬虫案例:Scrapy+XPath解析当当网网页结构
  • Lua(模块与包)
  • 【docker | 部署 】Jetson Orin与AMD平台容器化部署概述
  • Java 实现 B/S 架构详解:从基础到实战,彻底掌握浏览器/服务器编程
  • 前端性能新纪元:Rust + WebAssembly 如何在浏览器中实现10倍性能提升(以视频处理为例)
  • 【RAG优化】RAG应用中图文表格混合内容的终极检索与生成策略
  • VUE的学习
  • iOS WebView 加载失败与缓存刷新问题排查实战指南