HashMap底层原理:数据结构+put()流程+2的n次方+死循环+数据覆盖问题

2023-10-27

导航:

 

【Java笔记+踩坑汇总】Java基础+进阶+JavaWeb+SSM+SpringBoot+瑞吉外卖+SpringCloud+黑马旅游+谷粒商城+学成在线+MySQL高级篇+设计模式+常见面试题+源码_vincewm的博客-CSDN博客

目录

一、底层

1.1 HashMap数据结构

1.2 扩容机制

1.3 put()流程

1.4 HashMap是如何计算key的?

1.5 HashMap容量为什么是2的n次方?

1.5.1 原因

1.5.2 扩容均匀散列演示:从2^4扩容成2^5

二、线程安全问题

2.1 HashMap线程安全吗? 

2.2 线程安全的解决方案

2.3 JDK7扩容时死循环问题

2.3.1 死循环问题演示 

2.3.2 JDK8是怎么解决死循环问题的?

2.4 JDK8 put时数据覆盖问题

2.5 modCount非原子性自增问题


一、底层

1.1 HashMap数据结构

JDK1.7及之前版本,HashMap底层是“数组+单向链表”。

在JDK8中,HashMap底层是采用“数组+单向链表+红黑树”来实现的,使用红黑树主要是为了提高查询性能。数组用作哈希查找,链表用作链地址法处理冲突,红黑树替换长度为8的链表。

1.2 扩容机制

HashMap中,数组的默认初始容量为16,这个容量会以2的指数进行扩容。具体来说,当数组中的元素达到一定比例的时候HashMap就会扩容,这个比例叫做负载因子,默认为0.75。

自动扩容机制,是为了保证HashMap初始时不必占据太大的内存,而在使用期间又可以实时保证有足够大的空间。采用2的指数进行扩容,是为了利用位运算,提高扩容运算的效率。

数组每个元素存的是链表头结点地址,链地址法处理冲突,若链表的长度达到了8,红黑树代替链表。

1.3 put()流程

put()方法的执行过程中,主要包含四个步骤:

  1. 计算key存取位置,与运算hash&(2^n-1),实际就是哈希值取余,位运算效率更高。
  2. 判断数组,若发现数组为空,则进行首次扩容为初始容量16。
  3. 判断数组存取位置的头节点,若发现头节点为空,则新建链表节点,存入数组。
  4. 判断数组存取位置的头节点,若发现头节点非空,则看情况将元素覆盖或插入链表(JDK7头插法,JDK8尾插法)、红黑树。
  5. 插入元素后,判断元素的个数,若发现超过阈值则以2的指数再次扩容。

其中,第3步又可以细分为如下三个小步骤:

1. 若元素的key与头节点的key一致,则直接覆盖头节点。

2. 若元素为树型节点,则将元素追加到树中。

3. 若元素为链表节点,则将元素追加到链表中。追加后,需要判断链表长度以决定是否转为红黑树。若链表长度达到8、数组容量未达到64,则扩容。若链表长度达到8、数组容量达到64,则转为红黑树。

哈希表处理冲突:开放地址法(线性探测、二次探测、再哈希法)、链地址法

1.4 HashMap是如何计算key的?

key=value&(2^n-1) #结果相当于value%(2^n),使用位运算只要是为了提高计算速度。

例如当前数组容量是16,我们要存取18,那么就可以用18&15==2。相当于18%16==2.

put()里,计算key的部分源码:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        // 此处省略了代码
        // i = (n - 1) & hash]
        if ((p = tab[i = (n - 1) & hash]) == null)
            
            tab[i] = newNode(hash, key, value, null);
        
 
        else {
            // 省略了代码
        }
}

1.5 HashMap容量为什么是2的n次方?

1.5.1 原因

计算value对应key的Hash运算:

key=value&(2^n-1)#结果相当于value%(2^n)。例如18&15和18%16值是相等的

2^n-1和2^(n+1)-1的二进制除了第一位,后几位都是相同的。这样可以使得添加的元素均匀分布在HashMap的每个位置上,防止哈希碰撞。

例如15(即2^4-1)的二进制为1111,31的二进制为11111,63的二进制为111111,127的二进制为1111111。

1.5.2 扩容均匀散列演示:从2^4扩容成2^5

0&(2^4-1)=0;0&(2^5-1)=0

16&(2^4-1)=0;16&(2^5-1)=16。所以扩容后,key为0的一部分value位置没变,一部分value迁移到扩容后的新位置。

1&(2^4-1)=1;1&(2^5-1)=1

17&(2^4-1)=1;17&(2^5-1)=17。所以扩容后,key为1的一部分value位置没变,一部分value迁移到扩容后的新位置。

用取余演示扩容:

如果觉得与运算有点难理解,我们可以用取余演示:

假设从16扩容到32:1%16=1,17%16=1;1%32=1,17%32=17。

1和17本来key都是1,扩容后1的key还是1,17的key变成17。这样原来key为1的value就均匀的散列在扩容后的哈希表里了(一部分value不变,一部分value移动到扩容后新位置)。

二、线程安全问题

2.1 HashMap线程安全吗? 

HashMap是线程不安全的,多线程环境下可能出现死循环问题、数据覆盖问题。

多线程下建议使用Collections工具类和JUC包的ConcurrentHashMap。

2.2 线程安全的解决方案

  • 使用Hashtable(古老api不推荐)
  • 使用Collections工具类,将HashMap包装成线程安全的HashMap。
    Collections.synchronizedMap(map);
  • 使用更安全的ConcurrentHashMap(推荐),ConcurrentHashMap通过对槽(链表头结点)加锁,以较小的性能来保证线程安全。
  • 使用synchronized或Lock加锁HashMap之后,再进行操作,相当于多线程排队执行(比较麻烦,也不建议使用)。

2.3 JDK7扩容时死循环问题

2.3.1 死循环问题演示 

单线程扩容流程:

JDK7中,HashMap链地址法处理冲突时采用头插法,在扩容时依然头插法,所以链表里结点顺序会反过来。

假如有T1、T2两个线程同时对某链表扩容,他们都标记头结点和第二个结点,此时T2阻塞,T1执行完扩容后链表结点顺序反过来,此时T2恢复运行再进行翻转就会产生环形链表,即B.next=A; A.next=B,从而死循环。

2.3.2 JDK8是怎么解决死循环问题的?

JDK8 尾插法解决了死循环问题。

JDK8中,HashMap采用尾插法,扩容时链表节点位置不会翻转,解决了扩容死循环问题,但是性能差了一点,因为要遍历链表再查到尾部。 

例如A——>B——>C要迁移,迁移时先移动头结点A,再移动B并插入A的尾部,再移动C插入尾部,这样结果还是A——>B——>C。顺序没变,扩容线程

2.4 JDK8 put时数据覆盖问题

HashMap非线程安全,如果两个并发线程插入的数据hash取余后相等,就可能出现数据覆盖。

线程A刚找到链表null位置准备插入时就阻塞了,然后线程B找到这个null位置插入成功。借着线程A恢复,因为已经判过null,所以直接覆盖插入这个位置,把线程B插入的数据覆盖了。

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)     // 如果没有 hash 碰撞,则直接插入
            tab[i] = newNode(hash, key, value, null);
    }

2.5 modCount非原子性自增问题

modCount: HashMap的成员变量,用于记录HashMap被修改次数

put会执行modCount++操作,这步操作分为读取、增加、保存,不是一个原子性操作,也会出现线程安全问题。 

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
//put会执行modCount++操作,这步操作分为读取、增加、保存,不是一个原子性操作,也会出现线程安全问题。 
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

HashMap底层原理:数据结构+put()流程+2的n次方+死循环+数据覆盖问题 的相关文章

  • H.323,如何制作一个没有媒体的简单环。该脚本遵循 Q.931 设置,但仍然无法正常工作

    谁能帮我解决这个问题吗 当我发送此请求时 我在wireshark中看到数据包将发送到1720 tcp端口中的SJPhone 但 SJPhone 仍然没有响铃 我想让它响起 无论媒体 我非常感谢您的支持 我一定缺少消息协议细节来实现这个 请给
  • 使用正则表达式验证输入字符串是否为 0-255 之间的数字

    我在将输入字符串与正则表达式匹配时遇到问题 我想验证输入数字在 0 255 之间并且长度最多应为 3 个字符 代码工作正常 但当我输入 000000 至任意长度时 显示 true 而不是 false 这是我的代码 String IP 000
  • Java 流 - 按嵌套列表分组(按第二顺序列出)

    我有以下数据结构 每个学生都有一个州列表 每个州都有一个城市列表 public class Student private int id private String name private List
  • ResultSet:通过索引检索列值与通过标签检索

    使用 JDBC 时 我经常遇到这样的结构 ResultSet rs ps executeQuery while rs next int id rs getInt 1 Some other actions 我问自己 以及代码作者 为什么不使用
  • Java Spark DataFrameReader java.lang.NegativeArraySizeException

    学习 Spark for java 并尝试阅读 csv文件为DataFrame使用DataFrameReader 甚至不能得到一个超级简单的 csv文件工作 因为我不断收到异常java lang NegativeArraySizeExcep
  • EL 通过 Scriptlet

    在 JSP 中使用 EL 相对于 scriptlet 的优势是什么 EL 被认为是无脚本语言 EL 使 JSP 免受容易出错原始 Java 代码并强制您根据 MVC 思想编写 JSP EL 或像 JSTL 这样的标签库 不可能实现的任何事情
  • 全静态方法和应用单例模式有什么区别?

    我正在创建一个数据库来存储有关我的网站用户的信息 我正在使用 stuts2 因此使用 Java EE 技术 对于数据库 我将创建一个 DBManager 我应该在这里应用单例模式还是将其所有方法设为静态 我将使用这个 DBManager 进
  • 使用 Hibernate Dialect 设置表字符集/排序规则?

    我使用 Hibernate MySQLInnoDB Dialect 来生成 DDL hibernate cfg xml
  • AffineTransform.rotate() - 如何同时缩放、旋转和缩放?

    我有以下代码 它可以完成我想要绘制一个上面有一些棋子的棋盘的 第一部分 Image pieceImage getImage currentPiece int pieceHeight pieceImage getHeight null dou
  • MediaPlayer.create() 始终返回 null

    我以前用过媒体播放器 从来没有遇到过这个问题 每当我尝试使用 MediaPlayer create 时 该方法都会给我 null 并且我无法播放声音 我有什么遗漏的吗 public class Game extends Activity p
  • 我需要一个字数统计程序[关闭]

    这个问题不太可能对任何未来的访客有帮助 它只与一个较小的地理区域 一个特定的时间点或一个非常狭窄的情况相关 通常不适用于全世界的互联网受众 为了帮助使这个问题更广泛地适用 访问帮助中心 help reopen questions 我需要弄清
  • Java 中的 MP4 容器编写器

    我想找到一个免费的 Java MP4 容器 编写器 我不需要编码器 只需要能够根据预期值写入正确原子的编码器 Bonus对于这样一个库 也可以编写 有效 F4V 我更喜欢纯 Java 解决方案 而不是使用 JNI 或外部可执行文件的解决方案
  • 反应式 Spring Webflux REST 控制器内部重定向

    我正在为 spring 反应项目创建简单的控制器服务器 在设置重定向到另一个位置时 我在调用时发现错误http localhost 8080 There was an unexpected error type Internal Serve
  • selenium webdriver 中的多个程序执行不起作用

    Selenium WebDriver 中的多个程序执行不起作用 我编写了 1 个 testNG xml 文件和 2 个 java 类 我尝试从 xml 文件运行这两个 java 类 但这不起作用 XML代码
  • 两条腿的 OAuth 和 Gmail Atom feed

    我们正在尝试让 2 legged OAuth 与 Gmail Atom feed 一起使用 我们使用 John Kristian Praveen Alavilli 和 Dirk Ba lfanz 贡献的 Java 库 http oauth
  • Java 不可变对象 [关闭]

    Closed 这个问题需要多问focused help closed questions 目前不接受答案 我正在学习不变性的概念 据我了解 一旦创建对象 不可变对象就无法更改其值 但我不明白不可变对象的以下用途 They are 自动是线程
  • 获取包中声明的所有 Java 类的名称

    我正在编写一个功能 它将有助于将类放入我的程序的某个包中 另外 我只想要子类某个类的类 我需要这些类才能调用它们的静态方法 有没有一种自动的方法来做到这一点 如果是的话 速度慢吗 如果我不清楚 我想要的是这样的 ArrayList
  • 在 REST Web 服务中接受逗号分隔值

    我正在尝试接收 REST URI 中以逗号分隔值形式的字符串列表 示例 http localhost 8080 com vogella jersey first rest todo test 1 abc test 其中 abc 和 test
  • Struts2中的变量声明

    Struts2中如何声明变量并为该变量赋值 使用设置标签
  • 使用正则表达式匹配阿拉伯文文本

    我试图使用正则表达式仅匹配阿拉伯语文本 但出现异常 这是我的代码 txt matches P Arabic 这是例外情况 线程 main 中的异常 java util regex PatternSyntaxException 索引 9 附近

随机推荐

  • Ubuntu20.04+RTX3060+Nvidia驱动+cuda11.1+cudnn8.0.5

    Ubuntu20 04 RTX3060 Nvidia驱动配置过程 记录一下踩那么多坑之后的成功步骤 我下的Ubuntu的gcc版本为9 4 0 step1 apt get换源及更新 1 备份原本的源 cd etc apt cp source
  • 一个fb账号创建几个bm

    Facebook Business Manager 商务管理平台 是专为管理您的Facebook页面和广告帐户而设计的工具 通过使用商务管理平台 功能如下 管理对您的Facebook页面和广告帐户的访问权限 查看谁有权访问您的网页和广告帐户
  • Spring中最简单的过滤器和监听器

    1 过滤器概念引入 Filter也称之为过滤器 它是Servlet技术中最实用的技术 Web开发人员通过Filter技术 对web服务器管理的所有web资源 例如Jsp Servlet 静态图片文件或静态 html 文件等进行拦截 从而实现
  • UE4 蓝图之间交互

    小白欢迎评论 共同探讨 共同进步 获取其他蓝图 及蓝图内属性 的方法 有几种方法 下面来依次记录一下 根据不同情况可以适当选取一种合适的方法 1 两个普通蓝图类之间的直接交互 在蓝图类中申请公开变量 然后在外部赋值 即可交互 剩下就可以调用
  • 函数模板、模板函数,完全特例化、部分特例化

    一 函数模板 1 定义 建立一个通用函数 它所用到的数据的类型 包括返回值类型 形参类型 局部变量类型 可以不具体指定 而是用一个虚拟的类型来代替 实际上是用一个标识符来占位 等发生函数调用时再根据传入的实参来逆推出真正的类型 2 举例 t
  • accept函数笔记

    include
  • markdown 文本内跳转,生成目录

    生成目录的方法 一 数据集获取及预处理 1 1 数据集导入 1 1 2数据集划分 1 2 二 binary classification 二元分类器 2 自己实现交叉验证函数 2 1 confusion matrix 2 2 precisi
  • MySQL数据库的导入与导出

    1 数据库的导入 1 1 新建一个数据库名称 create database 数据库名 students 如下 create database students 1 2 使用use命令进入该数据库 如下 use students 1 3 导
  • 监控流媒体服务器的搭建和使用

    需求的提出 海康 大华 宇视等视频监控系统 都有自己的流媒体服务器平台 为什么要还需要通用的流媒体服务器产品呢 这个问题可以从几个方面回答 1 经济性 传统监控厂商的流媒体服务器 由于主要面向城市建设和大型安防项目 往往造价和报价相对较高
  • Android pm 命令详解

    一 pm命令介绍与包名信息查询 1 pm命令介绍 pm工具为包管理 package manager 的简称 可以使用pm工具来执行应用的安装和查询应用宝的信息 系统权限 控制应用 pm工具是Android开发与测试过程中必不可少的工具 sh
  • QT QString与char *之间的转换

    1 QString转char 先将QString转换为QByteArray 再将QByteArray转换为char QString str hello QString转char QByteArray ba str toLatin1 char
  • 启动过滤器异常

    org apache catalina core StandardContext filterStart 启动过滤器异常 org apache catalina core StandardContext filterStart 启动过滤器异
  • 【转载】自监督学习详细介绍(学习笔记)

    原文链接 https blog csdn net Cloris Sue article details 105343762 一 相关文献 fast ai上面关于自监督学习的资料 Self supervised learning and co
  • 云原生之使用docker部署ZPan个人网盘系统

    云原生之使用docker部署ZPan个人网盘系统 一 ZPan介绍 1 ZPan简介 2 ZPan特点 二 检查本地docker环境 1 检查系统版本 2 检查docker版本 3 检查docker服务状态 三 下载ZPan镜像 四 部署Z
  • MySQL根据某个字段判断新增或更新

    有一个 USER表 字段有 id username password email phone 我们需要开发一个创建用户接口 username唯一 判断username是否存在 如果存在 就更新 不存在 就新增 看一下正常代码 创建用户 pu
  • 动态数码管实验

    多位数码管简介 多位数码管 即两个或两个以上单个数码管并列集中在一起形成一体的数码管 当多位一体时 其内部的公共端是独立的 而负责显示什么数字的段线 a dp 全部是连接在一起的 独立的公共端可以控制多位一体中的哪一位数码管点亮 而连接在一
  • 1.linux系统基础笔记(互斥量、信号量)

    操作系统是很多人每天必须打交道的东西 因为在你打开电脑的一刹那 随着bios自检结束 你的windows系统已经开始运行了 如果问大家操作系统是什么 可能有的人会说操作系统就是windows 就是那些可以放大 缩小 移动的窗口 对曾经是计算
  • Jenkins & Harbor

    Harbor 环境搭建 https github com goharbor harbor releases tag v2 5 6 点击下载地址安装包 安装 解压安装包 root localhost tar zxvf harbor offli
  • #if、#else、#endif、#elif、#ifdef、#ifndef的区别和使用

    常用的条件编译 if elif else endif ifdef ifndef 看名字就知道 跟我们平时用的if elseif else是 一样的 不同的是这里一定要记得 endif if 条件 1 代码 1 elif 条件 2 代码 2
  • HashMap底层原理:数据结构+put()流程+2的n次方+死循环+数据覆盖问题

    导航 Java笔记 踩坑汇总 Java基础 进阶 JavaWeb SSM SpringBoot 瑞吉外卖 SpringCloud 黑马旅游 谷粒商城 学成在线 MySQL高级篇 设计模式 常见面试题 源码 vincewm的博客 CSDN博客