HashMap与ConcurrentHashMap详解:实现原理、源码分析与最佳实践

引言

在Java编程中,集合框架是最常用的工具之一,而HashMap和ConcurrentHashMap则是其中使用频率最高的两个Map实现。它们都用于存储键值对数据,但在实现机制、性能特点和适用场景上有着显著差异。

HashMap作为单线程环境下的首选Map实现,以其O(1)的访问效率和简洁API赢得了广泛应用;而ConcurrentHashMap则专为高并发环境设计,在保证线程安全的同时,提供了远优于传统同步集合的性能。

一、HashMap详解

1. 基本概念与特性

HashMap是Java集合框架中的一个核心类,实现了Map接口,基于哈希表的原理,提供了高效的插入和查询操作。它的主要特性包括:

  • 允许使用null作为键和值
  • 非线程安全
  • 不保证元素的顺序
  • 基本操作(get和put)的时间复杂度接近于常数时间

HashMap的底层实现是基于数组+链表+红黑树(JDK 1.8之后)的复合结构,这种设计既保证了查询效率,又解决了哈希冲突的问题。

2. 核心实现原理

2.1 存储结构演进

HashMap的存储结构随着JDK版本的更新而演进:

  在JDK 1.7及之前版本中,HashMap采用数组+链表的结构。每个数组元素称为一个桶(bucket),当发生哈希冲突时,同一个桶中的元素以链表形式存储。

  JDK 1.8引入了重大改进,当链表长度超过阈值(默认为8)时,链表会转换为红黑树,这大大提高了在哈希冲突严重情况下的查询效率。当红黑树节点数量小于6时,又会退化回链表,这是为了平衡空间和时间成本。

2.2 hash方法原理

HashMap中的hash方法是确定元素存储位置的关键,它的实现非常精妙:

static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

这个方法将key的hashCode值的高16位与低16位进行异或运算,目的是为了增加低位的随机性,减少哈希冲突。因为在确定桶位置时,只会用到哈希值的低位(与数组长度-1进行与运算),所以这种设计能够让高位的特征也参与到计算中来。

2.3 确定桶位置

HashMap通过(n - 1) & hash计算键值对在数组中的位置,其中n是数组的长度,hash是通过hash方法计算得到的哈希值。

这种方式等同于取模运算hash % n,但位运算的效率更高。为了使这种方式有效,HashMap的容量始终是2的幂次方,这样n-1的二进制表示全为1,进行与运算时能够充分利用哈希值的每一位。

3. 扩容机制

HashMap的扩容是一个重要且复杂的过程,直接影响到其性能表现。

3.1 扩容触发条件

  当HashMap中的元素数量超过负载因子与容量的乘积时,HashMap会进行扩容。默认情况下,初始容量是16,负载因子是0.75,这意味着当元素数量超过12时,会触发扩容。

3.2 扩容过程

扩容过程包括以下步骤:

  1. 创建一个新的数组,容量是原来的两倍
  2. 重新计算每个元素在新数组中的位置
  3. 将原数组中的元素转移到新数组中

在JDK 1.7中,扩容过程中的元素迁移是头插法,这在多线程环境下可能导致环形链表,从而引起死循环。

JDK 1.8对扩容过程进行了优化,采用尾插法,并且巧妙地利用了哈希值与旧容量的按位与运算,将元素分散到新桶的过程简化为:元素要么在原位置,要么在原位置+旧容量的位置。

3.3 负载因子的选择

负载因子是HashMap中一个重要的参数,它影响着空间利用率和查询效率之间的平衡:

  • 负载因子越大,空间利用率越高,但冲突的概率也越高,查询效率降低
  • 负载因子越小,冲突的概率越低,查询效率高,但空间利用率降低

默认的0.75是一个经过大量实验得出的比较合理的值,在时间和空间成本上取得了很好的平衡。在实际应用中,如果预知HashMap中元素的数量,可以在创建时指定初始容量,避免频繁扩容带来的性能损失。

4. 主要操作的实现

4.1 put操作

put操作是HashMap中最常用的方法之一,其实现逻辑如下:

  1. 如果HashMap未被初始化,则初始化
  2. 计算key的hash值
  3. 根据hash值确定在数组中的索引位置
  4. 如果该位置没有元素,直接插入
  5. 如果该位置有元素,遍历链表或红黑树
    • 如果找到相同的key,更新value
    • 如果没有找到相同的key,插入新节点
  6. 检查是否需要转换为红黑树(JDK 1.8)
  7. 检查是否需要扩容
4.2 get操作

get操作的实现相对简单:

  1. 计算key的hash值
  2. 根据hash值确定在数组中的索引位置
  3. 如果该位置没有元素,返回null
  4. 如果该位置有元素,遍历链表或红黑树,查找相同的key
    • 如果找到,返回对应的value
    • 如果没有找到,返回null
4.3 remove操作

remove操作的实现逻辑:

  1. 计算key的hash值
  2. 根据hash值确定在数组中的索引位置
  3. 如果该位置没有元素,返回null
  4. 如果该位置有元素,遍历链表或红黑树,查找相同的key
    • 如果找到,删除节点并返回对应的value
    • 如果没有找到,返回null

5. 线程不安全性分析

HashMap是非线程安全的,在多线程环境下可能出现各种问题:

  1. 在JDK 1.7中,并发扩容可能导致环形链表,从而引起死循环
  2. 并发put操作可能导致元素丢失
  3. 并发put和get操作可能导致get到脏数据

这些问题的根本原因是HashMap没有任何同步机制,多个线程同时修改HashMap的内部结构时会相互干扰。在多线程环境下,应该使用ConcurrentHashMap或者Collections.synchronizedMap()来代替HashMap。

二、ConcurrentHashMap详解

1. 基本概念与特性

ConcurrentHashMap是Java并发包中的重要成员,专为并发环境设计,提供了线程安全的Map实现。它的主要特性包括:

  • 线程安全,支持高并发访问
  • 不允许使用null作为键或值
  • 检索操作不需要锁定
  • 迭代器是弱一致性的,不会抛出ConcurrentModificationException
  • 提供了比Hashtable更好的并发性能

ConcurrentHashMap通过巧妙的设计,在保证线程安全的同时,最大程度地减少了锁竞争,提高了并发访问的效率。

2. 实现原理演进

ConcurrentHashMap的实现原理在JDK 1.7和JDK 1.8中有显著差异,体现了Java并发编程理念的演进。

2.1 JDK 1.7的分段锁机制

在JDK 1.7中,ConcurrentHashMap采用了分段锁(Segment)的设计:

  • 底层结构是Segment数组 + HashEntry数组 + 链表
  • 每个Segment相当于一个小型的HashMap
  • Segment继承自ReentrantLock,提供了锁的功能
  • 默认有16个Segment,支持16个线程并发写入
  • Segment的个数一旦初始化就不能改变

这种设计的核心思想是将数据分成多个段,每个段独立加锁,这样多个线程可以同时访问不同的段,提高了并发性能。

2.2 JDK 1.8的设计革新

JDK 1.8中,ConcurrentHashMap进行了重大改进,摒弃了分段锁的设计,采用了更加细粒度的锁定机制:

  • 底层结构与HashMap类似,是Node数组 + 链表 + 红黑树
  • 锁粒度更细,锁定单个桶(数组中的每个位置)
  • 使用CAS(Compare and Swap)操作和synchronized关键字保证线程安全
  • 当链表长度超过8时,转换为红黑树,提高查询效率

这种设计进一步减少了锁的粒度,提高了并发性能,同时简化了代码结构,使ConcurrentHashMap的实现更加优雅。

3. 并发控制机制

3.1 JDK 1.7的并发控制

在JDK 1.7中,ConcurrentHashMap的并发控制主要通过分段锁实现:

  • 对于写操作(put、remove等),需要先获取对应Segment的锁
  • 对于读操作(get等),不需要加锁,利用volatile关键字保证可见性
  • 不同Segment之间的写操作可以并行执行,提高了并发性能
3.2 JDK 1.8的并发控制

JDK 1.8中,ConcurrentHashMap的并发控制更加精细:

  • 初始化数组时使用CAS操作保证线程安全
  • 更新操作(put、remove等)使用synchronized锁定对应的桶
  • 读操作不需要加锁,利用volatile关键字保证可见性
  • 使用CAS操作进行计数和检查是否需要扩容
  • 支持多线程并发扩容,提高扩容效率

这种设计充分利用了JDK 1.8中synchronized的优化,以及CAS操作的无锁特性,在保证线程安全的同时,最大程度地提高了并发性能。

4. 主要操作的实现

4.1 put操作(JDK 1.8)

put操作是ConcurrentHashMap中最复杂的操作之一,其实现逻辑如下:

  1. 如果数组未初始化,先初始化数组
  2. 计算key的哈希值,确定在数组中的索引位置
  3. 如果该位置为空,使用CAS操作插入新节点
  4. 如果该位置的节点的哈希值为-1,说明正在扩容,则帮助扩容
  5. 否则,使用synchronized锁定该节点,进行后续操作:
    • 如果是链表,遍历链表查找相同的key
      • 如果找到,更新value
      • 如果没找到,插入新节点到链表尾部
    • 如果是红黑树,按照红黑树的方式插入节点
  6. 检查是否需要将链表转换为红黑树
  7. 增加计数,检查是否需要扩容
4.2 get操作(JDK 1.8)

get操作相对简单,不需要加锁:

  1. 计算key的哈希值,确定在数组中的索引位置
  2. 如果该位置为空,返回null
  3. 如果该位置的节点的key与查找的key相同,返回该节点的value
  4. 如果该位置是链表或红黑树,按照对应的数据结构查找节点
    • 如果找到,返回对应的value
    • 如果没找到,返回null
4.3 size操作

在JDK 1.8中,ConcurrentHashMap使用一个volatile变量baseCount和一个CounterCell数组来记录元素个数:

  • 在没有竞争的情况下,直接更新baseCount
  • 在有竞争的情况下,使用CounterCell数组分散计数
  • 获取size时,将baseCount和所有CounterCell的值相加

这种设计避免了全局锁定,提高了并发性能。

5. 扩容机制

5.1 JDK 1.7扩容

在JDK 1.7中,每个Segment独立扩容,扩容过程与HashMap类似:

  1. 创建一个新的HashEntry数组,容量是原来的两倍
  2. 遍历原数组中的每个元素,重新计算哈希值,放入新数组
  3. 将新数组赋值给Segment
5.2 JDK 1.8扩容

在JDK 1.8中,扩容过程更加复杂,但支持多线程并发扩容:

  1. 创建一个新的Node数组,容量是原来的两倍
  2. 将数组分成多个区段(stride),每个线程负责一个区段的迁移
  3. 使用ForwardingNode标记已经迁移完成的桶
  4. 当所有桶都迁移完成后,将新数组赋值给table属性

这种设计允许多个线程同时参与扩容过程,大大提高了扩容效率。

三、HashMap与ConcurrentHashMap对比分析

1. 线程安全性对比

  • HashMap:非线程安全,在多线程环境下可能导致死循环、数据丢失等问题
  • ConcurrentHashMap:线程安全,专为并发环境设计,通过精细的锁机制和CAS操作保证线程安全

2. 性能对比

  • HashMap:单线程环境下性能较好,没有同步开销
  • ConcurrentHashMap:在高并发环境下性能较好,但在单线程环境下由于同步开销,性能略低于HashMap

具体性能差异取决于并发程度、数据规模和操作类型。在实际应用中,应根据具体场景选择合适的实现。

3. 实现机制对比

特性HashMapConcurrentHashMap (JDK 1.8)
底层数据结构数组 + 链表 + 红黑树数组 + 链表 + 红黑树
线程安全机制synchronized + CAS
允许null键值
扩容机制单线程扩容多线程并发扩容
哈希冲突解决链表 + 红黑树链表 + 红黑树

3. 适用场景分析

  • HashMap适用于:

    • 单线程环境
    • 读多写少的场景
    • 对性能要求高的场景
    • 需要使用null键或值的场景
  • ConcurrentHashMap适用于:

    • 多线程并发环境
    • 读写都比较频繁的场景
    • 对线程安全有要求的场景
    • 需要较高并发性能的场景

四、最佳实践与性能优化

1. 合理设置初始容量

无论是HashMap还是ConcurrentHashMap,合理设置初始容量都能有效减少扩容次数,提高性能。如果能预估元素数量,应该在创建时指定合适的初始容量。

计算公式:initialCapacity = expectedSize / loadFactor + 1

2. 选择合适的负载因子

负载因子影响着空间利用率和查询效率,默认值0.75在大多数情况下是合适的。但在特定场景下,可以根据需求调整:

  • 如果内存充足,对时间效率要求高,可以降低负载因子
  • 如果内存紧张,对空间利用率要求高,可以提高负载因子

3. 避免频繁扩容

频繁扩容会导致性能下降,特别是对于大容量的Map。可以通过以下方式避免:

  • 预估元素数量,合理设置初始容量
  • 批量添加元素时,先计算最终容量,一次性扩容
  • 对于固定大小的数据集,可以禁用自动扩容

4. 合理使用多线程

在使用ConcurrentHashMap时,应该充分利用其并发特性:

  • 避免不必要的同步操作
  • 利用ConcurrentHashMap提供的原子操作方法
  • 合理设置并发级别,避免过多线程竞争

5. 常见陷阱与注意事项

  • 避免在多线程环境下使用HashMap
  • 注意ConcurrentHashMap不支持null键和值
  • 理解ConcurrentHashMap的弱一致性特性
  • 避免在迭代过程中修改Map结构
  • 注意自定义对象作为键时,必须正确实现hashCode()和equals()方法

总结

本文深入分析了HashMap和ConcurrentHashMap的实现原理、性能特点和适用场景。通过对比这两种重要的Map实现,我们可以看到Java集合框架在单线程和多线程环境下的不同设计思路。

HashMap凭借其简单高效的特性,在单线程环境中表现出色;而ConcurrentHashMap则通过精心设计的并发控制机制,在保证线程安全的同时,提供了优异的并发性能。在实际开发中,应根据具体场景选择合适的实现,并遵循最佳实践,以获得最佳性能。

理解这两种数据结构的内部工作原理,不仅有助于我们更好地使用它们,也能帮助我们在设计自己的数据结构时借鉴其中的优秀思想。

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

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

相关文章

CSS之动画(奔跑的熊、两面反转盒子、3D导航栏、旋转木马)

一、 2D转换 1.1 transform: translate( ) 转换(transform) 是CSS3中具有颠覆性的特征之一,可以实现元素的位移、旋转、缩放等效果 移动:translate 旋转:rotate 缩放:scale 下图为2D转换的坐标系 回忆…

【笔记】在 MSYS2(MINGW64)中安装 python-maturin 的记录

#工作记录 📌 安装背景 操作系统:MSYS2 MINGW64当前时间:2025年6月1日Python 版本:3.12(通过 pacman 安装)目标工具:maturin —— 用于构建和发布 Rust 编写的 Python 包 🛠️ 安装…

基于微信小程序的垃圾分类系统

博主介绍:java高级开发,从事互联网行业六年,熟悉各种主流语言,精通java、python、php、爬虫、web开发,已经做了六年的毕业设计程序开发,开发过上千套毕业设计程序,没有什么华丽的语言&#xff0…

工作日记之权限校验-token的实战案例

背景说明 我们组负责维护的一个系统,前端界面挂载在其他两个系统上,因为历史遗留原因,同时也挂在公网上,没有登陆功能和用户体系,只要输入网址就能访问,虽然这个系统是给公司内部人员使用,但是…

mysql双主模式下基于keepalived的虚拟ip实现高可用模式搭建

数据库安装和升级和双主配置的操作可以参考我的另一篇文章: 数据库安装和升级和双主配置 1、在两台服务器都下载和安装keepalived 下载: yumdownloader --resolve keepalived 下载后得到: [rootlocalhost keepalivedRpm]# ll 总用量 1896 …

展会聚焦丨漫途科技亮相2025西北水务博览会!

2025第三届西北水务数字化发展论坛暨供排水节水灌溉新技术设备博览会在兰州甘肃国际会展中心圆满落幕。本届展会以“科技赋能水资源,数智引领新动能”为主题,活动汇集水务集团、科研院所、技术供应商等全产业链参与者,旨在通过前沿技术展示与…

单调栈(打卡)

本篇基于b站灵茶山艾府。 下面是灵神上课讲解的题目与课后作业,课后作业还有三道实在写不下去了,下次再写。 739. 每日温度 给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是…

【机器学习基础】机器学习入门核心算法:层次聚类算法(AGNES算法和 DIANA算法)

机器学习入门核心算法:层次聚类算法(AGNES算法和 DIANA算法) 一、算法逻辑二、算法原理与数学推导1. 距离度量2. 簇间距离计算(连接标准)3. 算法伪代码(凝聚式) 三、模型评估1. 内部评估指标2. …

已有的前端项目打包到tauri运行(windows)

1.打包前端项目产生静态html、css、js 我们接下来用vue3 vite编写一个番茄钟案例来演示。 我们执行npm run build 命令产生的dist目录下的静态文件。 2.创建tarui项目 npm create tauri-applatest一路回车,直到出现。 3.启动运行 我们将打包产生的dist目录下的…

Unity3D仿星露谷物语开发55之保存地面属性到文件

1、目标 将游戏保存到文件,并从文件中加载游戏。 Player在游戏中种植的Crop,我们希望保存到文件中,当游戏重新加载时Crop的GridProperty数据仍然存在。这次主要实现保存地面属性(GridProperties)信息。 我们要做的是…

Java面试:企业协同SaaS中的技术挑战与解决方案

Java面试:企业协同SaaS中的技术挑战与解决方案 面试场景 在一家知名互联网大厂,面试官老王正在对一位应聘企业协同SaaS开发职位的程序员谢飞机进行技术面试。 第一轮提问:基础技术 老王:谢飞机,你好。首先&#xf…

SQL注入速查表(含不同数据库攻击方式与差异对比)

1. 字符串连接 字符串连接是SQL注入中常用的操作,用于将多个字符串拼接为一个,以构造复杂的注入语句。不同数据库的字符串连接语法存在显著差异,了解这些差异有助于精准构造payload。 Oracle:使用||操作符进行字符串连接&#xf…

uni-data-picker级联选择器、fastadmin后端api

记录一个部门及部门人员选择的功能,效果如下: 组件用到了uni-ui的级联选择uni-data-picker 开发文档:uni-app官网 组件要求的数据格式如下: 后端使用的是fastadmin,需要用到fastadmin自带的tree类生成部门树 &#x…

Mac电脑上本地安装 redis并配置开启自启完整流程

文章目录 一、安装 Redis方法 1:通过源码编译安装(推荐)方法 2:通过 Homebrew 安装(可选) 二、配置 Redis1. 创建配置文件和数据目录2. 修改配置文件 三、配置开机自启1、通过 launchd 系统服务&#xff08…

wsl安装linux

安装wsl 启用适用于 Linux 的 Windows 子系统 以管理员身份打开 PowerShell (> PowerShell > 右键单击 > 以管理员身份运行) 并输入以下命令,然后重启 dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsyste…

OpenGL 3D 编程

OpenGL 是一个强大的跨平台图形 API,用于渲染 2D 和 3D 图形。以下是 OpenGL 3D 编程的入门基础。 一. 环境设置 安装必要的库 GLFW: 用于创建窗口和处理输入 GLEW 或 GLAD: 用于加载 OpenGL 函数 GLM: 数学库,用于 3D 变换 // 基本 OpenGL 程序结构示例 #include <GL/g…

Android基于LiquidFun引擎实现软体碰撞效果

一、实现效果 Android使用LiquidFun物理引擎实现果冻碰撞效果 二、Android代码 // 加载liquidfun动态库static {System.loadLibrary("liquidfun");System.loadLibrary("liquidfun_jni");}class ParticleData {long id;ParticleSystem particleSystem;float…

Redis持久化机制详解:RDB与AOF的深度剖析

一、为什么需要持久化&#xff1f; Redis作为内存数据库&#xff0c;数据存储在易失性内存中。持久化机制解决两大核心问题&#xff1a; 数据安全&#xff1a;防止服务器宕机导致数据丢失灾难恢复&#xff1a;支持数据备份与快速重建 二、RDB&#xff1a;内存快照持久化 ▶ …

Netty学习example示例

文章目录 simpleServer端NettyServerNettyServerHandler Client端NettyClientNettyClientHandler tcp&#xff08;粘包和拆包&#xff09;Server端NettyTcpServerNettyTcpServerHandler Client端NettyTcpClientNettyTcpClientHandler protocolcodecCustomMessageDecoderCustomM…

ThreadLocal ,底层原理,强引用,弱引用,内存泄漏

目录 ThreadLocal的基本概念 底层实现原理 强引用与弱引用 内存泄漏问题 内存泄漏的解决方案 示例代码 ThreadLocal的基本概念 ThreadLocal是Java中的一个类&#xff0c;位于java.lang包下&#xff0c;它提供了线程局部变量的功能。每个使用该变量的线程都有自己独立的初…