炸裂,大神图解JDK容器三大将之——哈希表(HashMap)
liuian 2025-05-27 15:53 47 浏览
作者:JackpotDC
链接:
https://juejin.im/post/6861544032859127822
JDK容器三大将
任何一项新的技术、一种新的语言本质上都是算法+数据结构。任何技术的选型本质上都是在基于业务和硬件条件的充分理解,采用合适的数据结构、适当的算法以达到资源和效率的最优解。Java开发亦是如此,工欲善其事,必先利其器,想要使用Java这种语言开发好程序,就必须选择合适的数据结构来进行开发。JDK容器则是所有Java应用开发的基础,不论是业务代码,还是各种知名的Java开源项目(如异步网络框架netty、容器管理框架Spring、分布式计算框架Hadoop、搜索引擎框架Elastic Search),全都是大量在JDK容器的基础上进行开发。而JDK容器作为这些优秀开源项目的默认选择,必然有其优势,本文将分析下JDK容器三大将之哈希表。
JDK容器三大将:List、Set、Map
HashMap介绍
所有编程语言都躲不开数据结构原理中的几种经典结构,链表、线性表、哈希表等。哈希表以其O(1) 的查找耗时在查找速度方面傲视群雄,Java中哈希表的实现即HashMap类。 (原JDK中还有一个Hashtable类,由于put和get操作都加了synchronized锁导致单线程性能差,多线程又有基于分段锁的ConcurrentHashMap作为更好的选择,官方已经声明不再维护)
先来看下HashMap的继承关系图如下
- Map接口为实现类暴露了一系列方法,AbstractMap抽象类为Map的一些基础功能提供了简单实现
- Cloneable表示该类支持通过java.lang.Object#clone()方法克隆,clone方法相关的细节(浅拷贝、深拷贝)读者可以搜索相关关键字阅读
- Serializable接口表示该类支持Java自带序列化功能进行序列化
HashMap的数据结构
HashMap的数据结构如下图
下面分别来介绍下图中的几个概念:
- size,存着HashMap当前的大小,执行hashMap.size()时直接在O(1)的时间返回哈希表的大小,如图有着三个键值对,所以size=3
- modCount,记录哈希表结构变更的次数,用于在HashMap结构变更时能够快速失败,后面会详细介绍
- table,是一个Node数组(默认大小16),Node是HashMap的内部定义类,结构如图,保存着一对键值对。当执行map.put(k, v)命令时会根据k的哈希值映射到Node数组的某个位置,并且通过拉链法处理哈希冲突
- loadFactor、threshold,分别为装载因子(默认0.75)和阈值(Node.length * 装载因子),当HashMap的容量(即size)超过阈值时会触发rehash,对Node数组进行扩容
HashMap的put/get操作执行过程
put()
HashMap的put操作的执行过程伪代码如下
void put(key, value) {
index = hash(key);
if(table[index] == null)
table[index] = newNode(key, value); // 节点没有hash冲突时将该kv录入节点
else
insertIntoLastNode(table[index], key, value); // 节点hash冲突时写入拉链的最后
}
复制代码- 当hash冲突写入拉链时,HashMap有着一个常量TREEIFY_THRESHOLD=8,当拉链长度超过阈值后链表会升级为红黑树
- 上文提到过的参数threshold = Node.length * 装载因子,当put完的哈希表节点总数达到Node数组容量的0.75时会触发扩容
get()
HashMap的get操作执行过程的伪代码如下,都是经典的拉链法哈希查找的步骤
V get(key) {
index = hash(key);
if(table[index].key.hash == key.hash && table[index] == key)
return table[index].value; // 如上图中Node[2]定位到的第一个Node,如果对比相等,则返回value
else
return findNodeOrNullFromList(key); // 从拉链中查找该key的值
}
复制代码查找时同样也会需要根据当前是链表还是红黑树走不同的查询逻辑
HashMap的扩容过程
接下来重点看下HashMap的扩容过程,上文提到,HashMap在put的时候,如果put完的哈希表节点总数达到threshold,则会进行HashMap的扩容,扩容的操作过程如下图:
resize的展开过程如下:
- 开辟一个新的table数组,大小是原来的2倍,即table[32]
- 如果当前节点没有哈希冲突,则直接重新计算该节点的table[]数组下标位置并且放入
- 如果当前节点是红黑树,则执行红黑树的split操作将红黑树拆成两半,如果拆分后的大小小于了TREEIFY_THRESHOLD阈值的话,降级为链表
- 如果当前节点是链表,则根据当前节点的hash & oldTable.size(如本例中为16),根据结果为0(low链表)或1(high链表)拆分为两个链表,也就是根据16的二进制位(1_0000)从右往左数第5位
- low链表,即节点.hash & 16 == 0,即节点.hash第5位为0的节点保持原下标
- high链表,即节点.hash & 16 == 1,即节点.hash第5位为1的节点下标=原下标+16
下图展示了扩容时链表的大致拆分过程
HashMap的modCount以及使用注意
如前文所述,HashMap内部维护着一个成员变量modCount,在HashMap每次进行可能影响HashMap结构的操作时都会导致modCount++(例如put、remove) 这样做是因为HashMap是不支持线程安全的,如果你的代码在遍历HashMap的同时又在修改影响着HashMap的内容,必然会导致遍历出的结果不正确,与其拿到不正确结果导致后续基于这个错误结果的一系列错误,不如快速掀桌子抛出异常
ConcurrentModificationException结束这次遍历,这个就是快速失败(FailFast),其它相关的异常容错机制还有failover(失效转移)、failback(失效自动回复)等,感兴趣的读者可以自行搜索。
通过上文可以知道,modCount主要是遍历请求受到影响时的处理方式,但是我们的代码中经常会遇到一种经典的执行情况,如下
for(K key : map.keySet()) {
if(key == xxx)
map.remove(key); // 执行完后下一轮for循环抛出ConcurrentModificationException
}
复制代码这种时候我们是在一个单线程中,我们的目的是删掉符合判断条件的节点然后继续遍历Map,但是这样会导致代码抛出
ConcurrentModificationException异常,就是因为在执行了map.remove(key)之后modCount++,进而导致遍历开始前记录的 expectModCount != modCount,从而抛出异常 解决的办法是通过iterator.remove()删除节点
Iterator iter = map.entrySet().iterator();
while(iter.hasNext()) {
Entry entry = iter.next();
if(entry.key == xxx)
iter.remove();
}
复制代码HashMap的keySet和entrySet
在我们日常开发中经常需要对HashMap进行遍历,常用遍历方式有两种:keySet()和entrySet()。 这两种遍历方式返回的Set集合本质上都是HashMap的一个视图,这个Set本身是不存储数据的,只是它覆写了相关的iterator、contains等方法,这些方法又会去对HashMap中的table数组进行相关的查询等操作。 当我们对keySet()和entrySet()返回的Set集合进行add操作时会抛出
UnsupportedOperationException。
高性能的并发哈希表--ConcurrentHashMap
以上讨论的HashMap是JDK在Hashtable的改进上实现了高性能的单线程版的哈希实现,这在我们日常其实已经能够处理很多场景,甚至于当你所需的HashMap需要实现线程隔离的时候也可以通过ThreadLocal来实现(详见 图解分析ThreadLocal的原理与应用场景)
但是某些场景的哈希表不得不在多个线程之间共享,这些线程有可能同时读某一个key,同时改某一个key,一个在读某个key的时候另一个却在改这个key,面对这种情况HashMap只能掀桌子了,但是我们总还是需要一种支持多线程的高效的哈希数据结构,
ConcurrentHashMap:“没错,正式在下”。
关于ConcurrentHashMap的高并发哈希实现原理会在下篇文章分析。
相关推荐
- 163邮箱密码正确就是登不上(163邮箱密码一直错误)
-
邮箱不能登录或登录异常的原因有很多种哦,如您浏览器“隐私”或“安全”级别设置过高,或用户名、密码输入不正确、较长时间未登录被冻结等都会导致不能登录或登录异常。请您先检查一下哦。解决无法登录的方法有:...
- 移动硬盘维修费用大概是多少钱
-
芯片不需要多少钱,但数据恢复就另当别论了。。。如果认识人就帮你换个芯片板,要不了多少钱,如果是硬盘盒的芯片板坏了你就乾脆换个盒子,80左右。如果是硬盘芯片坏了,那就不好办了,没人愿意给你换阿。。。但如...
- windows资源管理器停止工作是什么原因
-
1.在进行重装系统之前,可以先检测一下windows资源管理器停止工作的原因是什么。如果是因为电脑的文件太多了,垃圾堆积导致的停止工作,我们就不需要进行重装系统。我们只需要下载一个360卫士或者其他可...
- 联想电脑24小时维修热线电话
-
1.打开Think.lenovo.com.cn网页,点击登陆。 2.输入用户名密码,点击登陆。 3.点击右上角的:返回个性化首页。 4.点击“咨询与报修”中的“网上报修”。 ...
- u盘上的系统怎么安装到电脑上
-
如果这个u盘是已经制作成为启动盘,可以进入pe系统的话就可以从u盘启动进入到pe系统中进行系统安装!如果你的意思是u盘里直接是操作系统的话,那就在bios设置里直接设定为u盘启动就好了!也可以在pe中...
- 20年前老笔记本改造升级(比较老的笔记本电脑改装)
-
答:10年前的笔记本电脑升级改造的方法。1.减少电脑后台程序。电脑和手机也是差不多的,有些软件在关闭之后并没有真正的退出,而是在后台偷偷的运行,这样也是占电脑内存,这样会导致电脑变得越来有。2....
- 住房公积金贷款计算器(住房公积金贷款计算器在线)
-
房贷、公积金贷款计算器基本养老保险金计算器基本医疗保险金计算器工伤保险计算器住房公积金缴存计算器养老保险退休金计算器五险一金及税后工资计算器失业保险计算器住房公积金贷款利息怎么计算,具体如下:公积金贷...
- 电脑开不了机风扇不转(电脑开机风扇转一会停了又继续转)
-
电脑开不了机,主机风扇转不动,出现这种情况有以下几种可能:1、电源线松了,或电源插板松动,又或者插板不通电。2、主机电源坏了。3、电脑的主板坏了。4、主机面的开关按钮坏了,或者按钮卡住了。解...
- 网页打不了怎么办(网页打不开是什么原因及解决方法)
-
浏览器打不开的修复方法: 步骤1、关于浏览器打不开的问题,首先点击电脑桌面左下角“开始”—>“运行”,输入regsvr32jscript.dll后选择“确定”,再次输入regsvr32vb...
- win10自带文件恢复工具(win10文件恢复工具推荐)
-
步骤:第一步:打开系统的管理员命令提示符窗口。Windows10系统打开管理员命令提示符窗口有如下几种方法:方法一:在系统桌面左下角的搜索栏输入:CMD,点击:命令提示符,可以打开管理员命令提示符窗口...
- 电脑本地磁盘c盘满了怎么办(电脑本地磁盘c盘满了如何删除)
-
当您的电脑本地磁盘C满了时,可能会出现一些问题,例如无法安装新程序、无法保存文件等。以下是一些解决方法:1.删除不需要的文件:可以通过手动删除不需要的文件或使用磁盘清理工具来清理本地磁盘C。在清理磁...
- 傲游浏览器(傲游浏览器app下载)
-
1、开始——程序——找到遨游——打开,如果能打开说明快捷方式有问题2、362急救箱系统修复、网络修复傲游浏览器曾经是一个备受推荐的浏览器,由于其强大的功能和用户友好的界面,在中国的浏览器市场占有一...
- 电脑怎么定时关机软件(电脑怎样定时开关机软件)
-
给电脑设置定时开关机的方法如下:1、点击桌面左下角的开始按钮,打开“控制面板”。2、然后我们点击“系统和安全3、点击下方的“管理工具”。4、再点击“任务计划程序”。5、点击“计划任务程序库”,选择“创...
- 一周热门
-
-
飞牛OS入门安装遇到问题,如何解决?
-
如何在 iPhone 和 Android 上恢复已删除的抖音消息
-
Boost高性能并发无锁队列指南:boost::lockfree::queue
-
大模型手册: 保姆级用CherryStudio知识库
-
用什么工具在Win中查看8G大的log文件?
-
如何在 Windows 10 或 11 上通过命令行安装 Node.js 和 NPM
-
威联通NAS安装阿里云盘WebDAV服务并添加到Infuse
-
Trae IDE 如何与 GitHub 无缝对接?
-
idea插件之maven search(工欲善其事,必先利其器)
-
如何修改图片拍摄日期?快速修改图片拍摄日期的6种方法
-
- 最近发表
- 标签列表
-
- python判断字典是否为空 (50)
- crontab每周一执行 (48)
- aes和des区别 (43)
- bash脚本和shell脚本的区别 (35)
- canvas库 (33)
- dataframe筛选满足条件的行 (35)
- gitlab日志 (33)
- lua xpcall (36)
- blob转json (33)
- python判断是否在列表中 (34)
- python html转pdf (36)
- 安装指定版本npm (37)
- idea搜索jar包内容 (33)
- css鼠标悬停出现隐藏的文字 (34)
- linux nacos启动命令 (33)
- gitlab 日志 (36)
- adb pull (37)
- python判断元素在不在列表里 (34)
- python 字典删除元素 (34)
- vscode切换git分支 (35)
- python bytes转16进制 (35)
- grep前后几行 (34)
- hashmap转list (35)
- c++ 字符串查找 (35)
- mysql刷新权限 (34)
