【JavaSE】深入HashMap

news2024/11/29 12:34:49

文章目录

  • 1. HashMap概述
  • 2. 哈希冲突
  • 3. 树化与退化
    • 3.1 树化的意义
    • 3.2 树的退化
  • 4. 二次哈希
  • 5. put方法源码分析
  • 6. key的设计
  • 7. 并发问题

参考

  • 如何防止因哈希碰撞引起的DoS攻击_hashmap dos攻击_双子孤狼的博客-CSDN博客
  • 为什么 HashMap 要用 h^(h >>>16) 计算hash值?槽位数必须是 2^n?_一行Java的博客-CSDN博客
  • HashMap面试,看这一篇就够了_苦味代码的博客-CSDN博客

1. HashMap概述

HashMap是Java中最常用的集合框架。

在JDK1.7的时候,HashMap的底层由数组和链表组成。数组是HashMap的主体,而链表是为了解决哈希冲突而存在的。

image-20230203164757901

在JDK1.8的时候,HashMap的底层由数组、链表和红黑树组成。红黑树的出现是为了解决因哈希冲突导致的链表长度过长影响HashMap性能的问题。红黑树搜索的空间复杂度为O(logn),而链表却是O(n)

也就是当链表的长度达到一定长度后,链表就会进行树化,当然这是一种笼统说法,具体细节待会深究。

img


2. 哈希冲突

哈希冲突是指对不同的关键字通过一个哈希函数进行计算得出相同的哈希值,这样使得它们存在的数组时候发生了冲突。

解决哈希冲突通常有以下四种方法。

  • 开放定址法

开放定址法,也称为再散列地址法。基本思想就是,如果p=H(key)出现冲突时,则以p为基础,再次hashp1=H(p),如果p1再次出现冲突,则以p1为基础,以此类推,直到找到一个不冲突的哈希地址pi

就是说当发生哈希冲突的时候,对哈希值进行求哈希值,只要哈希表足够大,那么总能找到一个这样的空地址。

因此开放定址法所需要的hash表的长度要大于等于所需要存放的元素。

  • 再哈希法

再哈希法,也称双重散列,多重散列。基本思想就是提供多个不同的哈希函数,当R1=H1(key1)发生冲突时,再计算R2=H2(key1),直到没有冲突为止。这样做虽然不易产生堆集,但增加了计算的时间。

  • 链地址法

链地址法,也称拉链法,将哈希值相同的元素构成一个同义词的单链表,并将单链表的头指针存放在哈希表的第i个单元中,查找、插入和删除主要在同义词链表中进行。链表法适用于经常进行插入和删除的情况。

  • 建立公共溢出区

将哈希表分为公共表和溢出表,当溢出发生时,将所有溢出数据统一放到溢出区。

HashMap采用的就是链地址法。

并且JDK1.7和JDK1.8的链表插入节点时,采用的方式不一样

  1. JDK1.7采用的是头插法。
  2. JDK1.8采用的是尾插法。

3. 树化与退化

树化是指将链表转化为红黑树的过程。

在JDK1.8的HashMap中,树化的规则是这样的:当链表长度超过树化阈值 8 时,先尝试扩容来减少链表长度,如果数组容量已经 >=64,才会进行树化

所以,HashMap并不是一开始就进行树化的。

阈值设置为8主要是因为泊松分布,具体原因HashMap作者在源码中也有解释

image-20230203172039652

意思就是说,理想情况下使用随机的哈希码,容器中节点分布在 hash 桶中的频率遵循泊松分布,按照泊松分布的计算公式计算出了桶中元素个数和概率的对照表,可以看到链表中元素个数为 8 时的概率已经非常小,再多的就更少了,所以原作者在选择链表元素个数时选择了 8,是根据概率统计而选择的。

也就是说哈希 值如果足够随机,则在 哈希表内按泊松分布,在负载因子 0.75 的情况下,长度超过 8 的链表出现概率是 0.00000006,树化阈值选择 8 就是为了让树化几率足够小。


3.1 树化的意义

首先,树化成红黑树可以避免DOS攻击,防止链表超长时性能下降,树化应当是偶然情况,是保底策略。

DOS攻击是指恶意的攻击者通过使用以下精心构造的数据,使得所有数据经过哈希函数之后,都映射到了一个位置,导致了大量数据出现了哈希冲突,通过HashMap查询的效率从O(1)变成了O(n)。这样就有可能发生因为查询操作消耗大量 CPU 或者线程资源,而导致系统无法响应其他请求的情况,从而达到拒绝服务攻击(DoS)的目的。

其次,哈希表的查找,更新的时间复杂度是 O(1),而红黑树的查找,更新的时间复杂度是 O(log2⁡n)

但是由于TreeNode 占用空间也比普通 Node 的大,如非必要,尽量还是使用链表。


3.2 树的退化

树的退化主要是发生在这两种情况下

  • HashMap在扩容时,如果拆分树,树元素个数小于等于6,则会退化成链表
  • remove 树节点时,若 rootroot.leftroot.rightroot.left.left 有一个为 null ,也会退化为链表

4. 二次哈希

在JDK1.8的源码的put方法中,会将key进行二次哈希

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

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

通过源码可以了解到,首先会调用key对象的hashCode方法进行求哈希值,然后将该哈希值的低16位与高16位进行异或运算,这样做的目的是为了综合高位数据,让哈希分布更为均匀,减少哈希碰撞。

并且这么做可以在数组 table 的 length 比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。

操作
hashCode()1,794,106,052
二进制01101010 11101111 11100010 11000100
h >>> 1600000000 00000000 01101010 11101111

5. put方法源码分析

查看put方法源码

transient Node<K,V>[] table;

public V put(K key, V value) {
    // 调用上文我们已经分析过的hash方法
    return putVal(hash(key), key, value, false, true);
}

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)
        // 第一次put时,会调用resize进行桶数组初始化
        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;
        }
    }
    ++modCount;
    if (++size > threshold)
        // 如果超过阈值,还需要扩容
        resize();
    afterNodeInsertion(evict);
    return null;
}

put方法的大体思路如下

  1. 调用keyhashCode方法计算哈希值,并据此计算出数组下标index
  2. 如果发现当前的桶数组为null,则调用resize()方法进行初始化
  3. 如果没有发生哈希碰撞,则直接放到对应的桶中
  4. 如果发生哈希碰撞,且节点已经存在,就替换掉相应的value
  5. 如果发生哈希碰撞,且桶中存放的是树状结构,则挂载到树上
  6. 如果碰撞后为链表,添加到链表尾,如果链表超度超过TREEIFY_THRESHOLD默认是8,则将链表转换为树结构
  7. 数据put完成后,如果HashMap的总数超过threshold就要resize

put方法中比较重要的一个知识点莫过于计算索引了。

  1. 首先,先调用hash方法。
    1. hash方法中,计算对象的hashCode方法
    2. 再进行调用HashMaphash()方法进行二次哈希
  2. 接着,将hash方法返回的二次哈希值记为hash,用(n - 1)也就是数组长度减一对hash进行与运算((n - 1) & hash

这里使用n-1是因为以默认数组长度16为例子,那么数组下标为0-15,哈希值计算hash%(2^4),其本质就是和长度取余。也就等价于 (2^4 - 1) & hash

在JDK1.8和JDK1.7中,它们的put方法实现有所不同

  1. 链表插入节点时,1.7 是头插法,1.8 是尾插法

  2. 1.7 是大于等于阈值且没有空位时才扩容,而 1.8 是大于阈值就扩容

  3. 1.8 在扩容计算 Node 索引时,会优化


6. key的设计

HashMap key 可以为 null,但 Map 的其他实现就不一定了

key应当符合下面的要求

  1. 作为 key 的对象,必须实现 hashCode equals,并且 key 的内容不能修改(不可变)
  2. key 的 hashCode 应该有良好的散列性

7. 并发问题

  • 扩容死链(1.7 会存在)
void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
        while(null != e) {
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}
  • enext 都是局部变量,用来指向当前节点和下一个节点
  • 线程1(绿色)的临时变量 enext刚引用了这俩节点,还未来得及移动节点,发生了线程切换,由线程2(蓝色)完成扩容和迁移

image-20210831084325075

  • 线程2 扩容完成,由于头插法,链表顺序颠倒。但线程1 的临时变量 e 和 next 还引用了这俩节点,还要再来一遍迁移

image-20210831084723383

  • 第一次循环
    • 循环接着线程切换前运行,注意此时 e 指向的是节点 a,next 指向的是节点 b
    • e 头插 a 节点,注意图中画了两份 a 节点,但事实上只有一个(为了不让箭头特别乱画了两份)
    • 当循环结束是 e 会指向 next 也就是 b 节点

image-20210831084855348

  • 第二次循环
    • next 指向了节点 a
    • e 头插节点 b
    • 当循环结束时,e 指向 next 也就是节点 a

image-20210831085329449

  • 第三次循环
    • next 指向了 null
    • e 头插节点 a,a 的 next 指向了 b(之前 a.next 一直是 null),b 的 next 指向 a,死链已成
    • 当循环结束时,e 指向 next 也就是 null,因此第四次循环时会正常退出

image-20210831085543224

  • 数据错乱(1.7,1.8 都会存在)

假设现在有两个线程A和B,这两个线程都分别将数据C和D放进HashMap中,并且C和D的二次哈希值都一样。

线程A和线程B同时执行到检查有无哈希冲突的那一段代码。A和B检查均无发现有哈希冲突。

假设线程A比较快,于是线程A将tab[i]指向数据C。

这时候线程B将tab[i]指向数据D。

最终tab[i]指向数据D,导致了数据C丢失


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

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

相关文章

动态代理是基于什么原理?

第6讲 | 动态代理是基于什么原理&#xff1f; 编程语言通常有各种不同的分类角度&#xff0c;动态类型和静态类型就是其中一种分类角度&#xff0c;简单区分就是语言类型信息是在运行时检查&#xff0c;还是编译期检查。 与其近似的还有一个对比&#xff0c;就是所谓强类型和弱…

ChatGPT is at capacity right now错误解决,ChatGPT问题汇总

我是老鱼&#xff0c;一名致力于在技术道路上的终身学习者、实践者、分享者&#xff01; 最近ChatGPT大火&#xff01;微软退出首款ChatGPT搜索引擎&#xff0c;阿里等国内巨头也纷纷爆出自家产品&#xff0c;一夜之间&#xff0c;全球最大的科技公司仿佛都回到了自己年轻时的…

CUDA By Example(八)——流

文章目录页锁定主机内存可分页内存函数页锁定内存函数CUDA流使用单个CUDA流使用多个CUDA流GPU的工作调度机制高效地使用多个CUDA流遇到的问题(未解决)页锁定主机内存 在之前的各个示例中&#xff0c;都是通过 cudaMalloc() 在GPU上分配内存&#xff0c;以及通过标准的C库函数 …

【数据结构】栈(stack)

写在前面本篇文章开始讲解栈的有关知识&#xff0c;其实把顺序表和链表学好&#xff0c;那么这一章便不在话下&#xff0c;栈实际上就是顺序表或链表的一些特殊情况。用顺序表实现的栈叫做顺序栈用链表实现的栈叫做链栈文章的内容分为几个部分&#xff0c;希望读者能快速了解文…

经过去年的一遍技术洗礼,《最新的Android 核心知识点》出炉了~

金三银四即将来临&#xff0c;相信很多人的心已经在开始蠢蠢欲动准备新年过后跳槽换一个好点的坑位了&#xff0c;披荆斩棘&#xff0c;斩关过将“杀掉”一众竞争对手 &#xff0c;最后成功靠着跳槽涨薪走上人生巅峰&#xff01; 理想很丰满现实慌得一批&#xff0c;大批大批的…

Docker进阶 - 6. docker network 网络模式之bridge

1. bridge概述 Docker 服务默认会创建一个 docker0 网桥(其上有一个 docker0 内部接口)&#xff0c;该桥接网络的名称为docker0&#xff0c;它在内核层连通了其他的物理或虚拟网卡&#xff0c;这就将所有容器和本地主机都放到同一个物理网络。Docker 默认指定了docker0接口的…

ucore的字符输出

ucore的字符输出有cga,lpt,和串口。qemu模拟出来显示器连接到cga中。 cga cga的介绍网站&#xff1a;https://en.wikipedia.org/wiki/Color_Graphics_Adapter cga是显示卡&#xff0c;内部有个叫6845的芯片。cga卡把屏幕划分成一个一个单元格&#xff0c;每个单元格显示一个a…

“数字孪生”:为什么要仿真嵌入式系统?

​01.仿真是什么&#xff1f; 仿真的概念非常广泛&#xff0c;但归根结底都是使用可控的手段来模仿真实的情况&#xff0c;通常应用于现实世界中实施难度大甚至是无法实践的事物。 众所周知&#xff0c;嵌入式系统通常是形式多样的、面向特定应用的软硬件综合体&#xff0c;无…

Web 框架 Flask 快速入门(一)flask基础与模板

前言 课程地址&#xff1a;Python Web 框架 Flask 快速入门 文章目录前言&#x1f334; Flask基础和模板&#x1f337; 一个简单的flask程序&#x1f33c; 模板的使用&#x1f334; Flask基础和模板 1、web框架的作用 避免重复造轮子&#xff0c;app程序不必关心于服务器的沟…

java agent设计开发概要

agent开发设计 agent 开发的一些心得&#xff0c;适合熟悉agent或者有agent开发需求的同学 1 有个基础的agent&#xff0c;是java 标准的agent。这是agent代码入口 2 设计包结构&#xff0c; 基础agent agent下有plugin,加载plugin可以自己定义一个类加载器 plugin&#xff1…

C++——多态 上

目录 一、概念 二、多态的定义及实现 三、动态多态的实现条件 四、重写相关 五、构成重写与同名隐藏的函数有什么区别&#xff1f; 六、C11 中的override 和 final 一、概念 通俗来说&#xff0c;就是多种形态&#xff0c;当完成某个行为时&#xff0c;当不同的对象去完…

Python 手写数字识别 MNIST数据集下载失败

目录 一、MNIST数据集下载失败 1 失败的解决办法&#xff08;经验教训&#xff09;&#xff1a; 2 亲测有效的解决方法&#xff1a; 一、MNIST数据集下载失败 场景复现&#xff1a;想要pytorchMINIST数据集来实现手写数字识别&#xff0c;首先就是进行MNIST数据集的下载&am…

Fastjson踩“坑”记录和“深度”学习

作者&#xff1a;陶征策 阿里国际站商家技术团队 Fastjson是阿里开发的Java语言编写的高性能JSON库&#xff0c;本文总结了Fastjson使用时的一些注意事项&#xff0c;并简单分析了Fastjson的底层工作原理&#xff0c;结合具体的验证代码以及跟Jackson的对比&#xff0c;希望能…

非代码的贡献也能成为Committer,我与DolphinScheduler社区的故事

点亮 ⭐️ Star 照亮开源之路 https://github.com/apache/dolphinscheduler ​ // 每个人对于”开源社区“的定义都不一样&#xff0c;在社区的想法也完全不一样&#xff0c;我认为玩开源就像”谈恋爱“&#xff0c;要想方设法对它好&#xff0c;在接触 Apache DolphinSche…

nodejs+vue毕业生求职招聘平台系统

前台首页功能模块毕业生信息招聘平台首页、空中宣讲会、招聘岗位、求职信息、论坛信息、试卷列表、招聘资讯、个人中心、后台管理功能。论坛中心试卷列表招聘资讯管理员功能模块管理员登录空中宣讲会管理招聘岗位管理毕业生管理企业功能模块招聘岗位管理信息咨询管理线上面试管…

java实现二叉树(一文带你详细了解二叉树的)

&#x1f387;&#x1f387;&#x1f387;作者&#xff1a; 小鱼不会骑车 &#x1f386;&#x1f386;&#x1f386;专栏&#xff1a; 《数据结构》 &#x1f393;&#x1f393;&#x1f393;个人简介&#xff1a; 一名专科大一在读的小比特&#xff0c;努力学习编程是我唯一…

JDBC与Druid连接池

1 什么是jdbc? JDBC&#xff08;java database connectivity &#xff09;是Java语言连接操作关系型数据库的一套解决方案&#xff0c;屏蔽了底层各数据库不同的差异。 具体是通过sun公司定义的统一的一套API【标准接口】来实现解决差异&#xff0c; 具体实现是各大数据库厂商…

【数据库】 mysql的四种安装方式

目录 MySQL 安装部署 一&#xff0c;仓库安装 二&#xff0c;本地安装 三&#xff0c;容器安装 四&#xff0c;源码安装 MySQL 安装部署 一&#xff0c;仓库安装 1&#xff0c;使用rpm命令从指定的网址装包 ​ [rootlocalhost yum.repos.d]# rpm -ivh https://repo.mys…

每日学术速递2.10

Subjects: cs.Cv 1.Spatiotemporal Deformation Perception for Fisheye Video Rectification 标题&#xff1a;鱼眼视频矫正的时空形变感知 作者&#xff1a;Shangrong Yang, Chunyu Lin, Kang Liao, Yao Zhao 文章链接&#xff1a;https://arxiv.org/abs/2302.03934v1 项…

【手写 Vuex 源码】第四篇 - Vuex 中 Getters 的实现

一&#xff0c;前言 上篇&#xff0c;主要介绍了 Vuex 中 State 状态的实现&#xff0c;主要涉及以下几个点&#xff1a; 创建 Store 类中的 State 状态&#xff1b;借助 Vue 实现 State 状态的响应式&#xff1b; 本篇&#xff0c;继续介绍 Vuex 中 getters 的实现&#xf…