面试题总结:HashMap底层原理

不仅仅是一道题,之后的某一天,它可能是破局的关键。

关于HashMap的知识点有哪些呢?分层次展示

1.基础知识:

存储键值对结构、底层数据结构、红黑树和链表

2.位运算与实现

位运算、put、get方法的实现

3.关于锁

segment锁和桶锁、线程不安全和HashTable、ConcurrentHashMap

1.关于HashMap的底层实现

HashMap的存储逻辑

HashMap的数据结构构成:数组+链表+红黑树(红黑树是jdk1.8才引入的)

JDK1.7 使用了数组+链表的方式
JDK1.8 使用了数组+链表+红黑树的方式

JDK8中HashMap引入了红黑树,同时在ConcurrentHashMap中做了相应优化。

ConcurrentHashMap是jdk1.5引入的

图摘自《HashMap的实现原理,源码深度剖析! – mikechen》

数组:transient Node<K,V>[] table 哈希桶数组

        其元素类型是Node,Node是HashMap的内部类,实现了Map.Entry接口(本质是键值映射)

        数组结构是Hash Table哈希表|散列表,根据key查找value :    value=table[hash(key)]

        数组长度总是2的n次方

核心问题:Hash冲突,Key不相同,但hash(Key)的值相同

        Hash冲突的解决方法有: 地址法、链地址法,此处采用链地址法

        就是在table数组本该放置元素的位置放置一个链表,把冲突的值链接在链表上,当链表过长时,将其转换为红黑树

链表转红黑树: static final int TREEIFY_THRESHOLD=8;  链表长度达到8时,转换为红黑树

红黑树转链表: static final int UNTREEIFY_THRESHOLD=6; 红黑树节点数减少到6个时,红黑树转换为链表

图摘自《HashMap和ConcurrentHashMap的区别,详解HashMap和ConcurrentHashMap数据结构-CSDN博客》

2.HashMap实现逻辑

(1).Hash函数的使用:

/**
计算key.hashCode()并将哈希值的高位数扩展(XORs)到低位数。
因为这个表使用了2的幂掩码,
在当前掩码之上只变化几比特的哈希集总是会发生冲突。(在已知的例子中,有一组浮点键在小表中保存连续整数。)因此,我们应用一个变换,将高比特的影响向下扩散。
比特传播的速度、效用和质量之间存在权衡。
因为许多常见的哈希集已经被合理地分布(所以不会从传播中受益),
因为我们用Tree来处理箱子bins里的大量碰撞,
我们只是用最便宜的方式对一些移位的位进行异或,以减少系统损失,
以及考虑最高位的影响,否则由于表边界的原因,索引计算中永远不会使用最高位
*/
static final int hash(Object key){int h,return (key==null)?0:(h=key.hashCode())^(h>>>16);
} 

key.hashCode()          Object hashCode() 方法用于获取对象的 hash 值。——>一个32位的int值

h^(h>>16)         计算key.hashCode()并将哈希值的高位数扩展(XORs)到低位数。且高位信息被保留在了低位信息中。 ——以代价更小的方式,减少碰撞。

                          

异或:           0^0=0  1^1=0 1^0=1 0^1=1

>>:                无符号右移

(2).数组槽位运算

(n-1)&hash

本身hash计算是取模计算,但是当且仅当数组长度是2的n次方时,使用&运算替代%运算可以提高效率。&比%效率更高

(3).put方法的操作流程

1.根据key计算hashcode,hashcode=hash(key)

2.使用Hash函数计算存储位置: index=mod(hashcode),对hashMap的容量取模

3.拿着index找到HashMap结构数组中的位置。

        (1)数组该位置为null或空: array[i]=null, 直接创建新节点,插入数据

        (2)数组该位置不为null或空:

                  HashMap采用拉链法解决hash冲突

                (a.)链表结构:

                        <8时遍历链表,创建新节点,头插法插入,

                        =8时遍历链表,头插法插入新节点后,将其转换为红黑树

                (b.)红黑树结构:(>8)遍历红黑树,创建新节点,插入新节点

                   若该key值已存在hashMap结构中,则对其值进行覆盖。

                    节点中存储的结构为Entry:  key-value键值对结构

4.插入成功后:

        当前HashMap结构中的节点数为size(HashMap含有的数据量大小)        

        size>threshold时,进行扩容。

        threshold=容量*装填因子=Capacity * LoadFactor 

        LoadFactor:HashMap负载因子|加载因子,为了降低哈希冲突的概率,默认当HashMap中的键值对达到数组大小的75%时,即会触发扩容。

​参考《【hashmap】HashMap原理及线程不安全详解|哈希表原理-CSDN博客》

参考《HashMap的实现原理,源码深度剖析! – mikechen》

(4).get方法的操作流程

1.根据key值计算hashcode,hashcode=hash(key)

2.对hashcode取模获得index,inde =mod(hashcode)

3.根据index找到Array的指定位置,此时可能匹配1-n个节点

4.遍历链表|红黑树的节点:获得一个节点的Entry结构,比较节点的key是否为查找到key

        是,返回该节点

        否,继续遍历

5.若遍历完都没找到要找的节点,则返回null

getOrDefault(key,默认值),找不到时返回指定默认值

(5).HashMap扩容

  • 当且仅当   数据大小>装填因子*容量,时触发扩容。

                如果不扩容,hash冲突的概率会逐渐提高,影响性能。

  • HashMap初始容量16,每次扩容×2,保证容量是2的幂次。
  • 扩容后所有元素需要重新计算位置:rehash()。
  • 这是因为:当且仅当容量是2的幂次时,mod取模操作可以替换为&操作,计算更高效。
  • 其次:容量是2的幂次,如16,二进制表示为1111。
  • 其余hashcode作与操作,获得index        index=hashcode&1111。

              index =  HashCode(Key) &  (Length - 1)

              index总是与最后四位有关系。

  • 其总是忠诚的反应后几位的值,此时:只要hashcode本身是符合均匀分布的,则index是符合均匀分布的。符合均匀分布可以减少hash冲突的次数。

参考《【hashmap】HashMap原理及线程不安全详解|哈希表原理-CSDN博客》

3.HashMap的锁与安全机制

(1).HashMap线程不安全-扩容死链

HashMap扩容并没有作线程安全的设置,故其是线程不安全的。会发生扩容死链,具体来说,如下:

扩容的两大步骤: 扩容、rehash, 扩容死链是在rehash时发生的。

假如:HashMap初始容量为16,装填因子0.75,当且仅当大于16*0.75=12时,触发扩容。

        此时,HashMap中有12个数据,再加入一个数据时,会出发扩容。

        此时,线程A和B同时加入一个新数据,此时,触发扩容。

        A扩容   B扩容: 创建扩容后的新数组

扩容的下一步是rehash操作,重新对数据进行分布。重新把元组加入到扩容后的数组上。

        1.线程B遍历到Entry3对象,执行完语句1,线程就被挂起。

                e = Entry3

                next = Entry2

        2.线程A畅通无阻地进行着Rehash,也执行了语句1:

                e = Entry3

                next = Entry2

        3.次数于语句2的i=3,对于两个线程来说都是正确的。

        4.此时采用头插法将元素进行插入时,有:

                A线程执行:

                        e.next=newTable[i]: 将Entry2指向Entry3

                        newTable[i]=e: 将Entry2放入newTable[i],此时完成头插法

                        e=next: 将Entry3的值赋值给e,此时: e=Entry3 next=Entry3

                B线程执行:

                        e.next=newTable[i]: 此时newTable[i]里是Entry2, e是Entry3,则此时Entry3指向Entry2

                        newTable[i]=e: 此时将Entry3再次放入newTable[i]

                        e=next: 此时Entry再次放入e中

                由于:

                Entry2指向Entry3,同时存在Entry3指向Entry2,形成环形结构,get查找指定数据时,会形成死循环

参考《【hashmap】HashMap原理及线程不安全详解|哈希表原理-CSDN博客》

图摘自《【hashmap】HashMap原理及线程不安全详解|哈希表原理-CSDN博客》

(2)ConcurrentHashMap如何保证线程安全

  1. JDK1.7版本中,ConcurrentHashMap的数据结构是由一个Segment数组和多个HashEntry组成,主要实现原理是实现了锁分离的思路解决了多线程的安全问题
  2. JDK1.8的实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本.
  3. JDK1.7版本的ReentrantLock+Segment+HashEntry
  4. JDK1.8版本中synchronized+CAS+HashEntry+红黑树

摘自《HashMap和ConcurrentHashMap的区别,详解HashMap和ConcurrentHashMap数据结构-CSDN博客》

(a).JDK1.7 ConcurrentHashMap锁分离的线程安全机制

JDK1.7 ConcurrentHashMap锁分离的线程安全机制:

其结构:一个Segment数组和多个HashEntry

ConcurrentHashMap 与HashMap和Hashtable 最大的不同在于:put和 get 两次Hash到达指定的HashEntry,第一次hash到达Segment,第二次到达Segment里面的Entry,然后在遍历entry链表. 

即:ConcurrentHashMap经历两次Hash, 而HashMap只有一次Hash

摘自《HashMap和ConcurrentHashMap的区别,详解HashMap和ConcurrentHashMap数据结构-CSDN博客》

1.第一次hash,找到Segment对应位置

Segment数组:

          将一个大的table分割成多个小的table来进行加锁——锁分离技术

          每一个Segment元素存储的是HashEntry数组+链表,(和HashMap结构一致)

Segment数组如何设置:

          

          初始化:通过位与运算来初始化,用ssize来表示, ssize也是2的幂次,

          DEFAULT_CONCURRENCY_LEVEL =16,限制了Segment的大小最多65536个

          Segment的大小默认16

2.第二次hash,找到对应键值对

          

          每一个Segment元素下的HashEntry的初始化也是按照位与运算来计算,用cap来表示

          HashEntry相当于HashMap结构,其容量是2的幂次(cap <<=1)

          HashEntry最小的容量为2   

     

put方法:

        Segment继承了ReentrantLock ,也就带有锁的功能

        1.第一次hash1(key)确定segment位置

                若segment未初始化,则CAS操作赋值

        2.第二次hash2(key),确定HashEntry的位置

                此处的操作受Segment继承的ReentrantLock影响

                线程试图获取锁:

                        成功获取锁: 插入数据

                        失败,其他线程占有锁:自旋的方式获取锁,超过指定次数挂起,等待唤醒

get方法和hashmap没有太大差别,只是经历了两次hash计算

size方法:由于并发操作,可能获取的size和真实值不一致。

                解决方案:     不加锁的模式下多次获取,三局两胜,最多三次

                                       上述不成功,则给每个Segment加上锁,计算size并返回.  

(b).JDK1.8 Synchronized和CAS来操作的线程安全机制

摘自《HashMap和ConcurrentHashMap的区别,详解HashMap和ConcurrentHashMap数据结构-CSDN博客》

结构:Node数组+链表+红黑树,其实就是针对红黑树的引入进行了修改

Node数据结构很简单,就是一个链表,但是只允许对数据进行查找,不允许进行修改

TreeNode继承于Node,但是数据结构换成了二叉树结构,它是红黑树的数据的存储结构,用于红黑树中存储数据,当链表的节点数大于8时会转换成红黑树的结构

摘自《HashMap和ConcurrentHashMap的区别,详解HashMap和ConcurrentHashMap数据结构-CSDN博客》

4.知识点补充

CAS:

CAS是Compare-And-Swap(比较并交换)的缩写,是一种轻量级的同步机制,主要用于实现多线程环境下的无锁算法和数据结构,保证了并发安全性。它可以在不使用锁(如synchronized、Lock)的情况下,对共享数据进行线程安全的操作。

自旋锁:

自旋锁是一种用于多线程编程的同步机制。它通过循环检查锁的状态来实现线程的等待和唤醒,而不是像互斥锁那样将线程阻塞。当一个线程尝试获取自旋锁时,如果锁已经被其他线程占用,该线程会一直循环检查锁的状态,直到锁被释放为止。

自旋锁的优点是在锁竞争不激烈的情况下,可以避免线程切换带来的开销,从而提高程序的性能。但是在锁竞争激烈的情况下,自旋锁可能会导致大量的空转,浪费CPU资源。

在实现上,自旋锁通常使用原子操作来实现对锁状态的操作,确保操作的原子性和线程安全性。

ReentrantLock:

ReentrantLock可重入的独占锁)是Java中的一个线程同步机制,它提供了与synchronized关键字类似的功能,但更加灵活和可扩展。ReentrantLock实现了Lock接口,可以用于实现更复杂的线程同步需求。

ReentrantLock的特点包括:

  1. 可重入性:同一个线程可以多次获取同一个锁,而不会造成死锁。
  2. 公平性:可以选择公平锁或非公平锁。公平锁会按照线程请求的顺序来获取锁,而非公平锁则允许插队。
  3. 条件变量:可以使用Condition对象来实现线程间的等待和通知机制。
  4. 中断响应:支持线程的中断响应,即在等待锁的过程中可以响应中断信号。

使用ReentrantLock需要注意以下几点:

  1. 在获取锁后,必须在finally块中释放锁,以确保锁的释放。
  2. 可以使用tryLock()方法尝试获取锁,如果获取失败则可以进行其他操作,而不是一直等待。
  3. 可以使用lockInterruptibly()方法来获取锁,在等待锁的过程中可以响应中断信号。

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

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

相关文章

Java 中 Set 集合是如何实现添加元素保证不重复的?

Java 中的 Set 集合是一种不允许包含重复元素的集合。它主要通过两种方式来实现确保元素不重复的机制&#xff1a;一是依赖元素的 hashCode() 方法和 equals() 方法&#xff0c;二是底层数据结构的支持。 1. hashCode() 和 equals() 方法 在 Java 中&#xff0c;每个对象都有…

【服务器配置】docker环境配置

docker环境配置 本文是在ubuntu 22.04机器配置docker环境 查看系统的内核版本 uname -a Linux xxf-ThinkStation-P340 5.15.0-101-generic #111-Ubuntu SMP Tue Mar 5 20:16:58 UTC 2024 x86_64 x86_64 x86_64 GNU/Linuxx86 64位 系统 如果是32位 不能安装docker 更新软件…

分布式数据库Polardb-X架构及特点

PolarDB-X架构 计算节点&#xff08;Compute Node&#xff0c;CN&#xff09;是系统的入口&#xff0c;采用无状态设计的sql引擎提供分布式路由和计算&#xff0c;包括SQL解析器、优化器、执行器等模块。负责数据分布式路由、计算及动态调度&#xff0c;负责分布式事务2PC协调…

基于java+springboot+vue实现的学生信息管理系统(文末源码+Lw+ppt)23-54

摘 要 人类现已进入21世纪&#xff0c;科技日新月异&#xff0c;经济、信息等方面都取得了长足的进步&#xff0c;特别是信息网络技术的飞速发展&#xff0c;对政治、经济、军事、文化等方面都产生了很大的影响。 利用计算机网络的便利&#xff0c;开发一套基于java的大学生…

文献学习-37-动态场景中任意形状针的单目 3D 位姿估计:一种高效的视觉学习和几何建模方法

On the Monocular 3D Pose Estimation for Arbitrary Shaped Needle in Dynamic Scenes: An Efficient Visual Learning and Geometry Modeling Approach Authors: Bin Li,† , Student Member, IEEE, Bo Lu,† , Member, IEEE, Hongbin Lin, Yaxiang Wang, Fangxun Zhong, Me…

PHP01——php快速入门 之 在Mac上使用phpstudy快速搭建PHP环境

PHP01——php快速入门 之 在Mac上使用phpstudy快速搭建PHP环境 0. 前言1. 下载小皮面板1.1 下载phpstudy&#xff08;小皮面板&#xff09;1.2 启动、简单访问1.2.1 启动Apache1.2.2 访问1.2.3 访问自定义文件或页面 2. 创建网站2.1 创建网站2.2 可能遇到的问题2.2.1 hosts权限…

Prompt提示工程上手指南:基础原理及实践-思维树 (ToT)策略下的Prompt

前言 此篇文章已经是本系列的第五篇文章&#xff0c;之前我们已经将检索增强生成(RAG)策略&#xff0c;逐渐我们掌握的知识和技术都在不断提高&#xff0c;对于Prompt的技巧策略也不能只局限于局部运用而要适应LLM大模型的整体框架去进行改进休整。较为主流的LLM模型框架设计基…

提升数据质量的三大要素:清洗prompt、数据溯源、数据增强(含Reviewer2和PeerRead)​

前言 我带队的整个大模型项目团队超过40人了&#xff0c;分六个项目组&#xff0c;每个项目组都是全职带兼职&#xff0c;且都会每周确定任务/目标/计划&#xff0c;然后各项目组各自做任务拆解&#xff0c;有时同组内任务多时 则2-4人一组 方便并行和讨论&#xff0c;每周文档…

Vue3实现pdf本地预览功能

一、先直接看看效果吧 放大后 缩小后 也可以分页显示 二、选用vue-pdf-embed和vue3-pdfjs的原因 选用这两个的插件是因为如果实现pdf预览其实使用iframe标签就可以的&#xff0c;但是使用iframe标签实现的比较臭&#xff0c;vue-pdf-embed是能够自定义样式的&#xff0c;更…

如何使用hugging face的模型库?

Hugging Face 是一个流行的自然语言处理 (NLP) 模型库和社区&#xff0c;提供了大量预训练模型、工具和资源&#xff0c;使得 NLP 的开发者和研究人员能够快速高效地构建和应用各种文本相关应用。在这里&#xff0c;我将向您介绍如何在 1 天内快速熟悉 Hugging Face 的基本功能…

分析系统性能问题从哪里入手?

本人十年大厂经验&#xff0c;整理技术资料不易&#xff0c; 完整详细文章关注公众号&#xff0c;后续还会有免费学习资料 1. 高性能架构的三个核心优化手段 负载均衡&#xff1a;通过分发用户请求到多个服务器&#xff0c;降低单一服务器的负载压力。 应用层负载均衡&#xf…

Python零基础从小白打怪升级中~~~~~~~TCP网络编程

TCP网络编程 一、什么是TCP协议 TCP( Transmission control protocol )即传输控制协议&#xff0c;是一种面向连接、可靠的数据传输协议&#xff0c;它是为了在不可靠的互联网上提供可靠的端到端字节流而专门设计的一个传输协议。 面向连接 &#xff1a;数据传输之前客户端和…

Stable Diffusion AI绘画宝典:从新手到高手,一图胜千言!

在这个数字化时代的浪潮中&#xff0c;人工智能技术以其惊人的创造力和创新性席卷全球。党的二十大报告把“实施科教兴国战略&#xff0c;强化现代化建设人才支撑”作为战略举措进行系统阐述&#xff0c;彰显我国不断发展新动能、新优势的决心和气魄。 Stable Diffusion是一款…

Vue3 + Element-Plus 使用 Table 预览图片发生元素遮挡

Vue3 Element-Plus 使用 Table 预览图片发生元素遮挡 问题代码问题重现解决方法最终效果 问题代码 <el-table-column label"视频" align"center"><template #default"scope" style"display: flex;"><div style"…

碾压LoRA!Meta CMU | 提出高效大模型微调方法:GaLore,内存可减少63.3%

引言 大模型训练通常会遇到内存资源的限制。目前常用的内存减少方法低秩适应&#xff08;LoRA&#xff09;&#xff0c;通过引入低秩&#xff08;low-rank&#xff09;适配器来更新模型的权重&#xff0c;而不是直接更新整个权重矩阵。然而&#xff0c;这种方法在预训练和微调…

消息队列和分布式消息队列

文章目录 分析系统现状不足中间件消息队列什么是消息队列&#xff1f;应用场景消息队列的模型为什么不直接传输&#xff0c;而要用消息队列&#xff1f;为什么要用消息队列&#xff1f;消息队列的缺点&#xff1f; 分布式消息队列分布式消息队列的优势&#xff1f;消息队列应用…

LeetCode55题:跳跃游戏(原创)

【题目描述】 给你一个非负整数数组 nums &#xff0c;你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。 判断你是否能够到达最后一个下标&#xff0c;如果可以&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 示例 1&am…

Oracle 19c补丁升级(Windows)

文章目录 一、打补丁前备份检查1、补丁包获取2、备份数据包以及数据库软件3、检查OPatch版本 二、补丁升级1、更新OPatch2、关闭监听以及服务3、补丁升级过程4、启动监听以及服务 三、数据库补丁应用 一、打补丁前备份检查 1、补丁包获取 补丁包&#xff1a; 百度网盘链接&am…

甘特图使用小诀窍,项目把控游刃有余

在项目管理过程中,掌握甘特图的使用技巧可以让你事半功倍,高效规划和监控项目进度。作为一种视觉化的工具,甘特图直观地展示了任务的开始和结束时间、持续时间以及任务之间的依赖关系,有助于预测和优化资源分配。掌握以下几个小诀窍,你就能驾驭甘特图,游刃有余地把控整个项目。…

运营商三要素验证API接口怎么对接

运营商三要素验证API接口又叫手机三要素验证API接口、运营商实名认证接口&#xff0c;这个接口是验证姓名、身份证号、手机号三者是否一致&#xff0c;返回验证结果&#xff0c;如果一致则说明三者信息匹配&#xff0c;可以有效确认当前注册用户的身份信息&#xff0c;那么运营…