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

news2024/9/24 19:17:04

导航:

 

【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;
    }

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

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

相关文章

电话号码的字母组合问题

解题思路&#xff1a; 当我第一眼看到这题的时候&#xff0c;我直接举出来一个列子“258”&#xff0c;直接套用多重for循环遍历可以罗列出来&#xff0c;但是根据数字组合的长度不能确定for循环的多少&#xff08;除非把所有for循环个数情况都罗列一遍&#xff09; 所以只能…

几种常用接口调用方式介绍

API&#xff0c;全称叫做Application Programming interface&#xff0c;也就是应用程序接口&#xff0c;API是一些预先定义的函数&#xff0c;我是学Java的&#xff0c;当我要使用这些函数的时候&#xff0c;便可以直接调用Java API&#xff0c;不用去访问源码&#xff0c;也不…

Linux设备驱动程序(四)——调试技术

文章目录 前言一、内核中的调试技术二、通过打印调试1、printk2、重定向控制台消息3、消息如何被记录4、开启及关闭消息5、速度限制6、打印设备编号 三、通过查询调试1、使用 /proc 文件系统①、在/proc中实现文件②、创建自己的 /proc 文件③、seq_file 接口 2、ioctl 方法 四…

Chatbot UI 和 ChatGLM2-6B 的集成

Chatbot UI 和 ChatGLM2-6B 的集成 0. 背景1. 部署 Chatbot UI2. 部署 ChatGLM2-6B3. 修改 ChatGLM2-6B 项目的 openai_api.py4. 修改 Chatbot UI 的配置5. 访问 Chatbot UI 0. 背景 尝试将 Chatbot UI 和 ChatGLM2-6B 的进行集成&#xff0c; ChatGLM2-6B 提供 API 服务&…

精确时钟同步协议ptp/IEEE-1588v2协议-------(2)主从时钟之间的消息交互与时钟同步过程

本文目录 1、主时钟和从时钟之间的消息交互流2、延时delay和偏移offset的计算2.1、延时delay的计算2.2、偏移offset的计算 主时钟和从时钟之间&#xff0c;通过sync, follow up, delay request, delay response这四条消息&#xff0c;完成时钟同步过程。PTP时钟同步系统能工作的…

word绘制横向表格

最近写小论文&#xff0c;表格太宽需要绘制横向表格&#xff0c;找了半天教程说的都不是很详细&#xff0c;我学习了一下决定自己写个教程。 我要在一和二之间创建一个横向表格。首先在一后面添加一个分节符号。布局->分隔符->分节负下一页。 再在二之前添加一个分节符号…

新耀东方|安全狗亮相2023第二届上海网络安全博览会

7月5日至7日&#xff0c;“新耀东方-2023第二届上海网络安全博览会暨高峰论坛”在上海顺利举办。此次大会由上海市信息网络安全管理协会、国家计算机网络应急技术处理协调中心上海分中心、(ISC)2上海分会、上海市普陀区科学技术委员会、上海市网络安全产业示范园共同主办。 作为…

左神算法之中级提升(2)

目录 [案例1】 【题目描述】 【思路解析1】 【思路解析2】 【代码实现】 【案例2】 【题目描述】 【思路解析】 【代码实现】 【案例3】 【题目描述】 【思路解析】 【代码实现】 【案例4】 【题目描述】今日头条2018面试题 第四题 【输入描述】 【思路解析】 【…

对于没有任何基础的初学者,云计算该怎样学习?

想学习任何一门专业技能&#xff0c;可以按下面这一套逻辑梳理&#xff01; 1&#xff09;了解基本内容 云计算这个技术是做什么的&#xff1f;适用哪些场景&#xff1f;有什么优点和缺点&#xff1f; 同时建议先找技术大纲&#xff0c;至少要学哪些技能点&#xff0c;可以网…

Layui之入门

目录 一、layui介绍 1.是什么 2.谁开发的 3.特点 二、layui&#xff0c;easyui和bootstrap的区别 1.layui、easyui与bootstrap的对比 2. layui和bootstrap对比&#xff08;这两个都属于UI渲染框架&#xff09; 3. layui和easyui对比 三、基础使用 四、登录注册实例讲解 …

医院陪诊小程序开发|陪诊小程序定制|陪诊服务app成品

陪诊小程序的功能开发对于陪诊行业有以下好处&#xff1a;   提高服务效率&#xff1a;陪诊小程序可以提供在线预约功能&#xff0c;方便用户随时预约合适的陪诊人员&#xff0c;减少了繁琐的人工沟通和安排工作&#xff0c;提高了服务效率。   增加服务范围&#xff1a;通…

基于matlab将图像标记器多边形转换为标记的块图像以进行语义分割(附源码)

一、前言 此示例演示如何将存储在对象中的多边形标签转换为适用于语义分割工作流的标记阻止图像。 可以使用计算机视觉工具箱中的图像标记器应用来标记太大而无法放入内存和多分辨率图像的图像。有关详细信息&#xff0c;请参阅在图像标记器&#xff08;计算机视觉工具箱&…

uniapp zjy-calendar日历,uni-calendar日历增强版

一、zjy-calendar简介 zjy-calendar日历是对uniapp uni-calendar日历的增强&#xff0c;支持圆点和文字自定义颜色。 二、使用方法 源使用说明&#xff1a;https://uniapp.dcloud.net.cn/component/uniui/uni-calendar.html 1、下载导入 https://ext.dcloud.net.cn/plugin?…

web-php

目录 基础 注释 php程序的组成 php的数据类型 php代码的运行 代码 显示时间 输出账户名和密码 后端对前端的数据进行验证处理代码 连接数据库的代码 前后端代码相结合验证&#xff0c;实现登录接口验证 login.html login.php register.html register.php error…

大模型调用工具魔搭GPT——一键调用魔搭社区上百个AI模型的API

为了让模型开发变得更容易,阿里云在发布会现场推出了一款令开发者耳目一新的工具:ModelScopeGPT(魔搭GPT)。它能够通过担任“中枢模型”的大语言模型一键调用魔搭社区其他的AI模型,实现大模型和小模型协同完成复杂任务。 这类智能调用工具被业界普遍看好。ModelScopeGP…

Android Handler被弃用,那么以后怎么使用Handler,或者类似的功能

Android API30左右&#xff0c;Android应用在使用传统写法使用Handler类的时候会显示删除线&#xff0c;并提示相关的方法已经被弃用&#xff0c;不建议使用。 Handler handler new Handler(){Overridepublic void handleMessage(NonNull Message msg) {super.handleMessage(…

分配操作菜单

目录 概述介绍数据库后端前端效果展示 概述 在写后台管理系统时, 我们可以根据不同的登录人,给予不同的功能菜单 如 :给楼栋管理员登录时分配(楼栋管理,宿舍管理) 所以在数据库就要创建: 1.登录人与角色表, 2再给角色表分配操作菜单 登录时查询对应的操作菜单,将数据响应给前端…

ASPICE软件工具链之Jira教程

Jira使用教程 一、什么是Jira? 二、Jira的使用教程 功能介绍: 创建工作流 工作流方案 设置字段流程 字段配置 界面方案 界面方案创建流程 问题类型界面方案 将项目与预先创建的方案关联 配置总流程 创建项目 设置项目 添加工作流 添加界面配置方案 设置Scrum 看板泳道图 一…

物联网行业的革命:Web3 技术如何改变我们的日常生活

物联网 (IoT) 是一个充满创新和潜力的领域&#xff0c;它将物理设备、传感器和互联网连接起来&#xff0c;实现智能化和自动化。 在过去几年中&#xff0c;从智能家居、智能城市到工业自动化&#xff0c;物联网技术已经渗透到了各个领域。然而&#xff0c;随着物联网设备和系统…

Spring源码系列-第1章-Spring源码纵览

必读 源码是循循渐进的&#xff0c;前面我会省略中间很多目前不需要深入的代码&#xff0c;所以会看起来代码比较少。省略的地方我会打上这样的标识 // ... 或者 // ...如果没打也不代表我没省略&#xff0c;可能是忘记了&#xff0c;不要看懵了。 第1章-Spring源码纵…