Java 集合 --- HashMap的底层原理

news2025/1/25 9:24:37

Java 集合 --- HashMap的底层原理

HashMap的下标计算

计算步骤

第一步: 计算hash值

  • 将h 和 h右移十六位的结果 进行XOR操作
  • 操作说明:
    高16位不动, 低16位与高16位做异或运算,
    也就是高十六位 + (低十六位 ^ 高十六位)
static final int hash(Object key) {
    int h;
    //hashCode()是native方法, 用 C/C++实现
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

第二步: 通过hash值计算下标值

  • 将hash值 和 hash数组减一 之后的数值进行AND操作
//n为 HashMap的长度
//这里 & 操作等同于取余操作
i = (n - 1) & hash 

Example:

  • HashMap的默认长度为16, 所以n-1这里取1111
  h = key.hashCode()     01101010 11101111 11100010 11000100
             h >>> 16    00000000 00000000 01101010 11101111 
------------------------------------------------------------
hash = h ^ (h >>> 16)    01101010 11101111 10001000 00101011
  (n - 1) = (2^4 - 1)    00000000 00000000 00000000 00001111
------------------------------------------------------------
     (2^4 - 1) & hash    00000000 00000000 00000000 00001011

为什么要 h ^ (h >>> 16)

假如没有做h ^ (h >>> 16)运算, 则hash的计算过程为:

hash = key.hashCode()    01101010 11101111 11100010 11000100
              (n - 1)    00000000 00000000 00000000 00001111
------------------------------------------------------------
     (n - 1) & hash =    00000000 00000000 00000000 00000100
  • 结合以上示例会发现,整个hash值,除了低四位参与了计算,其他全部没有起到任何的作用… 全部被0覆盖掉了.
  • 而大部分情况下, n的值(map的大小) 一般都会小于2^16次方,也就是65536. 则全部集中在低16位
  • 则如果key的hash值低位相同,计算出来的槽位下标都是同一个,大大增加了碰撞的几率;
  • 但如果使用h ^ (h >>> 16),将高位参与到低位的运算,整个随机性就大大增加了;
  • 结论: 增加离散性, 降低碰撞概率

为什么数组长度必须是2^n

增加离散性, 降低碰撞概率

  • 根据源码可知,无论是初始化,还是保存过程中的扩容,map的长度始终是2^n
  • 假如默认n的长度不是16(2^4),而是17,会出现什么效果呢?
hash           01101010 11101111 10001000 00101011
&
(17 - 1) = 16  00000000 00000000 00000000 00010000
----------------------------------------------
               00000000 00000000 00000000 00000000
  • 由于16的二进制是00010000,最终参与&(与运算)的只有1位,其他的值全部被0给屏蔽了;导致最终计算出来的下标只会是0或16.
  • 所以n的二进制值中必须尽可能多的出现1, 否则在&操作时不管hash值为多少都为0.
  • 二进制中出现1最多的数就是 2^n - 1

使用&替代 %, 提高计算效率

  • 还有一个原因是当 length = 2^n 时,X % length = X & (length - 1)
  • 具体数学推导: https://blog.csdn.net/Ricardo18/article/details/108846384
  • 而在计算机中 & 的效率比 % 高很多.

HashMap的树化

背景补充

  • JDK 1.7及之前HashMap的结构为: 数组 + 链表
  • Java7中Hashmap底层采用的是Entry对数组,而每一个Entry对又向下延伸是一个链表,在链表上的每一个Entry对不仅存储着自己的key/value值,还存了前一个和后一个Entry对的地址.
  • JDK 1.8: 数组+链表+红黑树
  • Java8中的Hashmap底层结构有一定的变化,还是使用的数组,但现在换成了Node对象(存储时也会存key/value键值对、前一个和后一个Node的地址),
  • 以前所有的Entry向下延伸都是链表,Java8变成链表和红黑树的组合,数据少量存入的时候优先还是链表,当链表长度大于8,且总数据量大于64的时候,链表就会转化成红黑树,
  • 所以你会看到Java8的Hashmap的数据存储是链表+红黑树的组合,如果数据量小于64则只有链表,如果数据量大于64,且某一个数组下标数据量大于8,那么该处即为红黑树

树化的条件

  • 条件一: 一个Node中链表的节点数量大于等于树化阈值 (也就是8). 源码如下
  • 必须满足第一个条件才能进入下一个条件
  • HashMap触发判断第一个条件的位置主要有4个方法,分别是putVal方法、computeIfAbsent方法、compute方法、merge方法
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
    treeifyBin(tab, hash);
  • 条件二: HashMap的Capacity大于等于最小树化容量值
  • 如果capacity小于64, 则选择扩容
  • 如果capacity大于等于64, 则进行树化
final void treeifyBin(Node<K,V>[] tab, int hash) {
	int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
    	//这里选择扩容
    	resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
    do {
        TreeNode<K,V> p = replacementTreeNode(e, null);
        if (tl == null)
        	hd = p;
        else {
            p.prev = tl;
            tl.next = p;
        }
        tl = p;
     } while ((e = e.next) != null);
     if ((tab[index] = hd) != null)
        hd.treeify(tab);
    }
}

总结

  • 如果数据量小于64则只有链表,如果数据量大于64,且某一个数组下标数据量大于8,那么该处即为红黑树

为什么链表变树的阈值为8

  • 在HashMap中, TreeNode的大小是普通node大小的两倍, 所以只有当链表里的node足够多时再树化 (平衡时间和空间) ’
  • 对于一个well-distributed的HashMap, node基本不会树化
  • HashMap的节点数量分布符合泊松分布:

0: 0.60653066
1: 0.30326533
2: 0.07581633
3: 0.01263606
4: 0.00157952
5: 0.00015795
6: 0.00001316
7: 0.00000094
8: 0.00000006

  • 链表长度为8的概率为0.00000006,在这种比较罕见和极端的情况下, 才会把链表转变为红黑树,转变为红黑树也是消耗性能的,是一个权衡的措施.
  • 当k=9时,也就是发生的碰撞次数为9次时,概率为亿分之三,碰撞的概率已经无限接近为0。
    如果设置为9,意味着,几乎永远都不会再次发生碰撞,基本永远都不会变树,因为概率太小了。因此设置为9,实在没必要。

为什么使用红黑树 而不是 AVL树

  • 红黑树相比avl树,在检索的时候效率其实差不多,都是通过平衡来二分查找。
  • AVL树是一种高度平衡的二叉树,所以查找的效率非常高,但是,有利就有弊,AVL树为了维持这种高度的平衡,就要付出更多代价。每次插入、删除都要做调整,就比较复杂、耗时。所以,对于有频繁的插入、删除操作的数据集合,使用AVL树的代价就有点高了。
  • 红黑树只是做到了近似平衡,并不严格的平衡,所以在维护的成本上,要比AVL树要低
  • 所以,红黑树的插入、删除、查找各种操作性能都比较稳定。对于工程应用来说,要面对各种异常情况,为了支撑这种工业级的应用,我们更倾向于这种性能稳定的平衡二叉查找树。在现在很多地方都是底层都是红黑树的天下.

HashMap的扩容

背景补充:

  • HashMap的初始容量16, 每次扩容都是两倍, 所以HashMap的容量必为2的n次幂
  • 负载因子默认为0.75, 也就是当数组的密度大于百分之75时会进行扩容
  • 当链表的长度大于8, 但数组长度小于64的时候, 会进行扩容
  • JDK1.7在链表中的插入方式为头插法, 这样可以避免遍历链表, 但是在多线程的情况下会有死循环问题, JDK1.8则改为尾插法
  • JDK1,7 中部会新建一个数组,然后通过transfer方法, 将原数组中的键值对依次加入到新数组中。transfer方法会遍历旧数组,对于每个数组元素,会遍历其中的每个节点,并重新计算其hash值,然后使用头插法将其插入到新的索引位置上

JDK1.8的扩容方式 – 不需要重新计算hash的值
第一步:

  • 由于扩容直接加了1倍,因此相当于length-1原来的最右侧的0变为了1, 比如:
16 -> 32
16-1 = 15   0000000000000000 0000000000001111
32-1 = 31   0000000000000000 0000000000011111

第二步:

  • 下标的计算是 hash & (n-1), 所以新下标取决于变为1的那个bit所对应的hash中的bit是0还是1
长度为16 = 10000
hash        xxxxxxxxxxxxxxxx xxxxxxxxxxxyxxxx
16 - 1      0000000000000000 0000000000001111

下标为xxxx
长度为32 = 100000
hash        xxxxxxxxxxxxxxxx xxxxxxxxxxxyxxxx
16 - 1      0000000000000000 0000000000011111

如果y = 1 则新下标为 1xxxx 也就是 10000 + xxxx = 原来的capacity + 原位置
如果y = 0 则新下标不变, 为 xxxx
  • 这个设计非常的巧妙,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。这一块就是JDK1.8新增的优化点

HashMap的put流程

  • 首先判断table是否为空或者length = 0
  • 如果是则进行扩容
  • 通过key计算下标位置
  • 如果node为空直接插入
  • 如果node不为空(说明发生冲突)
  • 如果为TreeNode则插入红黑树
  • 如果为node则判断链表是否大于8, 小于8直接插入链表
  • 大于8则进行树化
  • 加入新元素后, 判断是否需要扩容, 然后结束
    在这里插入图片描述

HashMap的线程安全问题

  • HashMap是线程不安全的
  • 线程安全的hashmap为 ConcurrentHashMap, 或者HashTable (不常用)

多线程下 put 会导致元素丢失

  • 多线程同时执行 put 操作时,如果计算出来的索引位置是相同的,那会造成前一个 key 被后一个 key 覆盖,从而导致元素的丢失

put 和 get 并发时会导致 get 到 null

  • 线程 A 执行put时,因为元素个数超出阈值而出现扩容,线程B 此时执行get。当线程 A 执行完 table = newTab 之后,线程 B 中的 table 此时也发生了变化,此时去 get 的时候就会 get 到 null 了,因为元素还没有迁移完成

JDK1.7中头插法带来的死循环问题

https://blog.csdn.net/littlehaes/article/details/105241194

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

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

相关文章

Java的dump文件分析及JProfiler使用

Java的dump文件分析及JProfiler使用 1 dump文件介绍 从软件开发的角度上&#xff0c;dump文件就是当程序产生异常时&#xff0c;用来记录当时的程序状态信息&#xff08;例如堆栈的状态&#xff09;,用于程序开发定位问题。 idea配置发生OOM的时候指定路径生成dump文件 # 指定…

关于upstream的八种回调方法

1 creat_request调用背景&#xff1a;用于创建自己模板与第三方服务器的第一次连接步骤1&#xff09; 在Nginx主循环&#xff08;ngx_worker_process_cycle方法&#xff09; 中&#xff0c;会定期地调用事件模块&#xff0c; 以检查是否有网络事件发生。2&#xff09; 事件模块…

智慧校园平台源码 智慧教务 智慧电子班牌系统

系统介绍 智慧校园系统是通过信息化手段,实现对校园内各类资源的有效集成 整合和优化&#xff0c;实现资源的有效配置和充分利用&#xff0c;将校务管理过程的优化协调。为校园提供数字化教学、数字化学习、数字化科研和数字化管理。 致力于为家长和教师提供一个全方位、多层…

ChatGPT的出现网络安全专家是否会被替代?

ChatGPT的横空出世&#xff0c;在业界掀起了惊涛骇浪。很多人开始担心&#xff0c;自己的工作岗位是否会在不久的将来被ChatGPT等人工智能技术所取代。网络安全与先进技术发展密切相关&#xff0c;基于人工智能的安全工具已经得到很多的应用机会&#xff0c;那么未来是否更加可…

关于表格表头和第一列固定

主要是使用黏性布局 position: sticky; top: 0;//left:0; 来实现 给test-table一个固定的宽和高 然后trauma-table开启inline-flex布局&#xff08;注意不能用flex布局 否则 trauma-table的宽度不能被子元素撑起来 会滚动到一定宽度后吸左侧的效果就没有了&#xff09;&am…

Spring 用到了哪些设计模式

关于设计模式&#xff0c;如果使用得当&#xff0c;将会使我们的代码更加简洁&#xff0c;并且更具扩展性。本文主要讲解Spring中如何使用策略模式&#xff0c;工厂方法模式以及Builder模式。1. 策略模式关于策略模式的使用方式&#xff0c;在Spring中其实比较简单&#xff0c;…

【每日一题】缓存穿透、缓存击穿、缓存雪崩及解决方案

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站。 当下ChatGPT很火&#xff0c;让人心痒痒想试一试好不好用&#xff0c;因此我就试着借它写一篇文章&#xff0c;但是试了几次最终还是没有…

电子技术——负反馈特性

电子技术——负反馈特性 本节我们进一步深入介绍负反馈特性。 增益脱敏性 假设 β\betaβ 是一个常数。考虑下面的微分方程&#xff1a; dAfdA(1Aβ)2dA_f \frac{dA}{(1 A\beta)^2} dAf​(1Aβ)2dA​ 将上式除以 AfA1AβA_f \frac{A}{1A\beta}Af​1AβA​ 得到&#xff1…

LDAP使用docker安装部署与使用

一、准备工作本地或者服务器中安装dockerapt update apt install -y docker.io systemctl restart docker将openLDAP的docker镜像拉取到本地。镜像拉取命令&#xff1a;docker pull osixia/openldap将openLDAP的可视化管理工具phpldapadmin的镜像拉取到本地。镜像拉取命令&am…

来香港一年的感悟与随笔

再过三周&#xff0c;来港就满一年了。 人生就是这样&#xff0c;有时候很微妙&#xff1a;在小木虫看到老板的信息&#xff0c;两封邮件一次面试&#xff0c;我就来香港了。 这一年里的感悟和变化&#xff0c;主要在对科学研究的认识以及对人生与选择的感悟和回顾这两个方面。…

GUI可视化应用开发及Python实现

0 建议学时 4学时&#xff0c;在机房进行 1 开发环境安装及配置 1.1 编程环境 安装PyCharm-community-2019.3.3 安装PyQt5 pip install PyQt5-tools -i https://pypi.douban.com/simple pip3 install PyQt5designer -i https://pypi.douban.com/simple1.2 环境配置 选择“…

Elasticsearch集群内存占用高?用这招!

一、freeze index冻结索引介绍 Elasticsearch为了能够实现高效快速搜索&#xff0c;在内存中维护了一些数据结构&#xff0c;当索引的数量越来越多&#xff0c;那么这些数据结构所占用的内存也会越来越大&#xff0c;这是一个不可忽视的损耗。 在实际的业务开展过程中&#x…

实战——缓存的使用

文章目录前言概述实践一、缓存数据一致1.更新缓存类2.删除缓存类二、项目实践&#xff08;商城项目&#xff09;缓存预热双缓存机制前言 对于我们日常开发的应用系统。由于MySQL等关系型数据库读写的并发量是有一定的上线的&#xff0c;当请求量过大时候那数据库的压力一定会上…

3717: yuyu学数数

描述yuyu开始学数数了&#xff0c;她要爸爸给他一些火柴棍&#xff0c;她要拼出很多数来。yuyu每次说要拼什么数字&#xff0c;爸爸就得想想要给她几根&#xff0c;好累啊&#xff0c;于是就只好写程序了。输入输入数据有多组&#xff0c;每组占一行&#xff0c;每行一个非负整…

K_A12_030 基于STM32驱动Pulse sensor心率模块 上位机与OLED0.96双显示

K_A12_030 基于STM32驱动Pulse sensor心率模块 上位机与OLED0.96双显示一、资源说明二、基本参数参数引脚说明三、驱动说明STM32 ADC采集:四、部分代码说明1、接线引脚定义STM32F103C8T6Pulse sensor心率模块五、基础知识学习与相关资料下载六、视频效果展示与程序资料获取七、…

计算机内存数值存储方式-原码、反码、补码

计算机内存数值存储方式 1、原码 一个数的原码(原始的二进制码)有如下特点&#xff1a; ①最高位做为符号位&#xff0c;0表示正,为1表示负 ②其它数值部分就是数值本身绝对值的二进制数 ③负数的原码是在其绝对值的基础上&#xff0c;最高位变为1 下面数值以1字节的大小描述…

Nginx——Nginx的基础原理

摘要 Nginx 是俄罗斯人编写的十分轻量级的 HTTP 服务器,是一个高性能的HTTP和反向代理服务器&#xff0c;同时也是一个 IMAP/POP3/SMTP 代理服务器。Nginx 是由俄罗斯人 Igor Sysoev 为俄罗斯访问量第二的 Rambler.ru 站点开发的&#xff0c;它已经在该站点运行超过两年半了。…

Jetson Xavier nx(ubuntu18.04)安装rtl8152网卡驱动和8192网卡驱动

含义 Bus 002 : 指明设备连接到哪条总线。 Device 003 : 表明这是连接到总线上的第二台设备。 ID : 设备的ID&#xff0c;包括厂商的ID和产品的ID&#xff0c;格式 厂商ID&#xff1a;产品ID。 Realtek Semiconductor Corp. RTL8153 Gigabit Ethernet Adapter:生产商名字和设备…

力扣-寻找用户推荐人

大家好&#xff0c;我是空空star&#xff0c;本篇带大家了解一道简单的力扣sql练习题。 文章目录前言一、题目&#xff1a;584. 寻找用户推荐人二、解题1.正确示范①提交SQL运行结果2.正确示范②提交SQL运行结果3.正确示范③提交SQL运行结果4.其他总结前言 一、题目&#xff1a…

详解Linux多线程中锁、条件变量、信号量

一文读懂Linux多线程中互斥锁、读写锁、自旋锁、条件变量、信号量 Hello、Hello大家好&#xff0c;我是木荣&#xff0c;今天我们继续来聊一聊Linux中多线程编程中的重要知识点&#xff0c;详细谈谈多线程中同步和互斥机制。 同步和互斥 互斥&#xff1a;多线程中互斥是指多个…