当前位置: 首页 > news >正文

【JavaSE】深入HashMap

文章目录

  • 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 1sttreeifyBin(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丢失


相关文章:

【JavaSE】深入HashMap

文章目录1. HashMap概述2. 哈希冲突3. 树化与退化3.1 树化的意义3.2 树的退化4. 二次哈希5. put方法源码分析6. key的设计7. 并发问题参考 如何防止因哈希碰撞引起的DoS攻击_hashmap dos攻击_双子孤狼的博客-CSDN博客 为什么 HashMap 要用 h^(h &#xff1e;&#xff1e;&#…...

华为机试题:HJ62 查找输入整数二进制中1的个数(python)

文章目录博主精品专栏导航知识点详解1、input()&#xff1a;获取控制台&#xff08;任意形式&#xff09;的输入。输出均为字符串类型。1.1、input() 与 list(input()) 的区别、及其相互转换方法2、print() &#xff1a;打印输出。1、整型int() &#xff1a;将指定进制&#xf…...

代码随想录训练营一刷总结|

分为几个大部分&#xff1a; 数组 最先接触的部分&#xff0c;虽然说感觉是最简单的&#xff0c;但是需要掌握好基础&#xff0c;特别是小心循环。这里面需要再仔细看的就是螺旋矩阵那一块&#xff0c;其他的在后续刷的时候能用一种方法一次a就行。 链表 需要注意链表的基础…...

CSS中的几种尺寸单位

一、尺寸单位 CSS 支持多种尺寸单位&#xff0c;包括&#xff1a; px&#xff1a;像素&#xff0c;固定大小单位em&#xff1a;相对于当前元素字体大小的单位rem&#xff1a;相对于根元素&#xff08;HTML&#xff09;字体大小的单位%&#xff1a;相对于父元素的百分比单位vh…...

运维必会:ansible剧本(piaybook)

playbooks 概述以及实例操作 Playbooks 组成部分&#xff1a; Inventory Modules Ad Hoc Commands Playbooks Tasks: 任务&#xff0c;即调用模块完成的某些操作 Variables: 变量 Templates: 模板 Handlers: 处理器&#xff0c;由某时间触发执行的操作 Roles: 角色 YAML 介绍…...

活动星投票午间修身自习室制作在线投票投票制作网页

“午间修身自习室”网络评选投票_免费小程序投票推广_小程序投票平台好处手机互联网给所有人都带来不同程度的便利&#xff0c;而微信已经成为国民的系统级别的应用。现在很多人都会在微信群或朋友圈里转发投票&#xff0c;对于运营及推广来说找一个合适的投票小程序能够提高工…...

C#泛型:高级静态语言的效率利器

文章目录引入类型约束子类泛型常用的泛型数据结构前文提要&#xff1a; &#x1f48e;超快速成&#xff0c;零基础掌握C#开发中最重要的概念&#x1f48e;抽丝剥茧&#xff0c;C#面向对象快速上手&#x1f48e;Winform&#xff0c;最友好的桌面GUI框架&#x1f48e;懂了委托&a…...

澳大利亚访问学者申请流程总结

澳大利亚访问学者申请需要注意些什么&#xff1f;下面知识人网小编整理澳大利亚访问学者申请流程总结。1、取得wsk英语成绩&#xff0c;现在都是先买票再上车了。2、联系外导&#xff0c;申请意向接收函(email)。3、向留学基金委CSC提出申请。4、获批后&#xff0c;申请正式邀请…...

cookie和Session的作用和比较

目录 什么是cookie cookie的工作原理 什么是session Session的工作原理 为什么会有session和cookie cookie和session如何配合工作 cookie和Session作用 什么是会话 什么是cookie cookie是web服务器端向我们客户端发送的一块小文件&#xff0c;该文件是干嘛的呢&#xf…...

测试员都是背锅侠?测试人员避“锅”攻略,拿走不谢

最近发生了一起生产事故&#xff0c;究其根源&#xff0c;事故本身属于架构或者需求层面需要规避的问题&#xff0c;测试人员的责任其实是非常小的&#xff0c;但实际情况是&#xff1a;相关测试人员因此承担了很大的压力&#xff0c;成为质量问题的“背锅侠”。 实际上&#…...

C++: C++模板<template>

C template content&#x1f60a;前言&#x1f601;模板&#x1f495;1、泛型编程&#x1f60d;2、函数模板&#x1f612;2.1&#xff1a;函数模板概念&#x1f44c;2.2&#xff1a;函数模板的格式&#x1f618;2.3&#xff1a;函数模板原理&#x1f601;2.4&#xff1a;函数模…...

chmod命令详解

用法&#xff1a;chmod [选项]… 模式[,模式]… 文件…  或&#xff1a;chmod [选项]… 八进制模式 文件…  或&#xff1a;chmod [选项]… --reference参考文件 文件… Change the mode of each FILE to MODE. With --reference, change the mode of each FILE to that of R…...

状态机设计中的关键技术

⭐本专栏针对FPGA进行入门学习&#xff0c;从数电中常见的逻辑代数讲起&#xff0c;结合Verilog HDL语言学习与仿真&#xff0c;主要对组合逻辑电路与时序逻辑电路进行分析与设计&#xff0c;对状态机FSM进行剖析与建模。 &#x1f525;文章和代码已归档至【Github仓库&#xf…...

单片机开发---ESP32S3移植NES模拟器(二)

书接上文 《单片机开发—ESP32-S3模块上手》 《单片机开发—ESP32S3移植lvgl触摸屏》 《单片机开发—ESP32S3移植NES模拟器&#xff08;一&#xff09;》 暖场视频&#xff0c;小时候称这个为—超级曲线射门&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&am…...

微信小程序nodej‘s+vue警局便民服务管理系统

本文首先介绍了设计的背景与研究目的,其次介绍系统相关技术,重点叙述了系统功能分析以及详细设计,最后总结了系统的开发心得在Internet高速发展的今天,我们生活的各个领域都涉及到计算机的应用,其中包括“最多跑一次”微信小程序的网络应用,在外国小程序的使用已经是很普遍的方…...

第18章 MongoDB $type 操作符教程

MongoDB $type 操作符 描述 在本章节中&#xff0c;咱们将继续讨论MongoDB中条件操作符 $type。 $type操作符是基于BSON类型来检索集合中匹配的数据类型&#xff0c;并return 结果。 MongoDB 中可以使用的类型如下表所示&#xff1a; 类型数字备注Double1 String2 Object3…...

【MySQL主从复制】快速配置

本文配置环境Windows和Linux。 windows主 Linux 从 一、主库配置 首先保证Linux和防火墙开启3306端口或关闭防火墙。 登录Mysql管理员账户: GRANT REPLICATION slave,reload,super ON *.* TO root@从库ip地址 IDENtIFIED BY root; flush privileges; 本地的mysql可以被:…...

Typescript - interface 关键字(通俗易懂的详细教程)

前言 简单来说&#xff0c;Interface 就是一种描述对象或函数的东西。 您可以把 interface 理解为形状&#xff0c;真实开发情况下&#xff0c;一个对象需要有什么样的属性&#xff0c;函数需要什么参数或返回什么样的值&#xff0c;数组应该是什么样子的&#xff0c;一个类和继…...

【计组】内存和总线

课程链接&#xff1a;深入浅出计算机组成原理_组成原理_计算机基础-极客时间 一、虚拟内存和内存保护 日常使用的操作系统下&#xff0c;程序不能直接访问物理内存。内存需要被分成固定大小的页&#xff08;Page&#xff09;&#xff0c;再通过虚拟内存地址&#xff08;Virtu…...

CUDA中的数学方法

CUDA中的数学方法 文章目录CUDA中的数学方法1. Standard FunctionsSingle-Precision Floating-Point FunctionsDouble-Precision Floating-Point Functions2. Intrinsic FunctionsSingle-Precision Floating-Point FunctionsDouble-Precision Floating-Point Functions参考手册…...

Debian系统简介

目录 Debian系统介绍 Debian版本介绍 Debian软件源介绍 软件包管理工具dpkg dpkg核心指令详解 安装软件包 卸载软件包 查询软件包状态 验证软件包完整性 手动处理依赖关系 dpkg vs apt Debian系统介绍 Debian 和 Ubuntu 都是基于 Debian内核 的 Linux 发行版&#xff…...

循环冗余码校验CRC码 算法步骤+详细实例计算

通信过程&#xff1a;&#xff08;白话解释&#xff09; 我们将原始待发送的消息称为 M M M&#xff0c;依据发送接收消息双方约定的生成多项式 G ( x ) G(x) G(x)&#xff08;意思就是 G &#xff08; x ) G&#xff08;x) G&#xff08;x) 是已知的&#xff09;&#xff0…...

解锁数据库简洁之道:FastAPI与SQLModel实战指南

在构建现代Web应用程序时&#xff0c;与数据库的交互无疑是核心环节。虽然传统的数据库操作方式&#xff08;如直接编写SQL语句与psycopg2交互&#xff09;赋予了我们精细的控制权&#xff0c;但在面对日益复杂的业务逻辑和快速迭代的需求时&#xff0c;这种方式的开发效率和可…...

连锁超市冷库节能解决方案:如何实现超市降本增效

在连锁超市冷库运营中&#xff0c;高能耗、设备损耗快、人工管理低效等问题长期困扰企业。御控冷库节能解决方案通过智能控制化霜、按需化霜、实时监控、故障诊断、自动预警、远程控制开关六大核心技术&#xff0c;实现年省电费15%-60%&#xff0c;且不改动原有装备、安装快捷、…...

Spring Boot+Neo4j知识图谱实战:3步搭建智能关系网络!

一、引言 在数据驱动的背景下&#xff0c;知识图谱凭借其高效的信息组织能力&#xff0c;正逐步成为各行业应用的关键技术。本文聚焦 Spring Boot与Neo4j图数据库的技术结合&#xff0c;探讨知识图谱开发的实现细节&#xff0c;帮助读者掌握该技术栈在实际项目中的落地方法。 …...

MinIO Docker 部署:仅开放一个端口

MinIO Docker 部署:仅开放一个端口 在实际的服务器部署中,出于安全和管理的考虑,我们可能只能开放一个端口。MinIO 是一个高性能的对象存储服务,支持 Docker 部署,但默认情况下它需要两个端口:一个是 API 端口(用于存储和访问数据),另一个是控制台端口(用于管理界面…...

Caliper 配置文件解析:fisco-bcos.json

config.yaml 文件 config.yaml 是 Caliper 的主配置文件,通常包含以下内容: test:name: fisco-bcos-test # 测试名称description: Performance test of FISCO-BCOS # 测试描述workers:type: local # 工作进程类型number: 5 # 工作进程数量monitor:type: - docker- pro…...

libfmt: 现代C++的格式化工具库介绍与酷炫功能

libfmt: 现代C的格式化工具库介绍与酷炫功能 libfmt 是一个开源的C格式化库&#xff0c;提供了高效、安全的文本格式化功能&#xff0c;是C20中引入的std::format的基础实现。它比传统的printf和iostream更安全、更灵活、性能更好。 基本介绍 主要特点 类型安全&#xff1a…...

Python 高效图像帧提取与视频编码:实战指南

Python 高效图像帧提取与视频编码:实战指南 在音视频处理领域,图像帧提取与视频编码是基础但极具挑战性的任务。Python 结合强大的第三方库(如 OpenCV、FFmpeg、PyAV),可以高效处理视频流,实现快速帧提取、压缩编码等关键功能。本文将深入介绍如何优化这些流程,提高处理…...

【Veristand】Veristand环境安装教程-Linux RT / Windows

首先声明&#xff0c;此教程是针对Simulink编译模型并导入Veristand中编写的&#xff0c;同时需要注意的是老用户编译可能用的是Veristand Model Framework&#xff0c;那个是历史版本&#xff0c;且NI不会再维护&#xff0c;新版本编译支持为VeriStand Model Generation Suppo…...