面试题总结: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.mfbz.cn/a/547153.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

OpenCV基本图像处理操作(三)——图像轮廓

轮廓 cv2.findContours(img,mode,method) mode:轮廓检索模式 RETR_EXTERNAL &#xff1a;只检索最外面的轮廓&#xff1b;RETR_LIST&#xff1a;检索所有的轮廓&#xff0c;并将其保存到一条链表当中&#xff1b;RETR_CCOMP&#xff1a;检索所有的轮廓&#xff0c;并将他们组…

广东莱斯广告,6.8米UV喷印推动粤东喷绘产业升级

广东莱斯广告作为汕头市大型的广告服务运营商,近日迎来了一件值得庆祝的事情:彩神6.8米UV喷印机运行一周年,销售服务商深圳嘉豪总经理李伟特地前来回访。该设备是深圳润天智数字设备股份有限公司开发的全球首台搭载XTRA6800H柯尼卡喷头的设备,设备特点是:1.色彩艳丽;2.超宽喷印…

Suno,属于音乐的ChatGPT时刻来临

AI绘画 AI视频我们见过了&#xff0c;现如今AI都能生成一首音乐&#xff0c;包括编曲&#xff0c;演唱&#xff0c;而且仅需几秒的时间便可创作出两分钟的完整歌曲 相信关注苏音的很大一部分都是从获取编曲或者混音插件来的&#xff0c;现如今AI却能帮你几秒生成曲子 今天就带…

Postgresql源码(125)游标恢复执行的原理分析

问题 为什么每次fetch游标能从上一次的位置继续&#xff1f;后面用一个简单用例分析原理。 【速查】 恢复扫描需要知道当前页面、上一次扫描到的偏移位置、当前页面一共有几条&#xff1a; 当前页面&#xff1a;HeapScanDesc结构中记录了扫到的页面&#xff08;scan->rs_cb…

计算机网络:数据链路层 - CSMA/CA协议

计算机网络&#xff1a;数据链路层 - CSMA/CA协议 CSMA/CA概述帧间间隔工作原理退避算法虚拟载波监听 CSMA/CA概述 讲解CSMA/CA之前&#xff0c;我们回顾一下CSMA/CD的三个特性&#xff1a; 多址接入MA&#xff1a;多个主机连接在一条总线上&#xff0c;竞争使用总线 载波监听…

2024年主流的java混淆工具有哪些

2024年&#xff0c;主流的Java混淆工具可能会包括&#xff1a; ProGuard&#xff1a;ProGuard 是一个免费的开源 Java 混淆工具&#xff0c;可用于压缩、优化和混淆 Java 字节码。它是Android开发者的首选混淆工具之一&#xff0c;并且在Java应用程序中也得到了广泛应用。 Dex…

Stable Diffusion超详细教程!从0-1入门到进阶

一、本地部署 Stable Diffusion 前言 目前市面上比较权威&#xff0c;并能用于工作中的AI绘画软件其实就两款。一个叫Midjourney&#xff08;简称MJ&#xff09;&#xff0c;另一个叫Stable-Diffusion&#xff08;简称SD&#xff09;。MJ需要付费使用&#xff0c;而SD开源免费…

【洛谷 P8802】[蓝桥杯 2022 国 B] 出差 题解(带权无向图+单源最短路+Dijkstra算法+链式前向星+最小堆)

[蓝桥杯 2022 国 B] 出差 题目描述 A \mathrm{A} A 国有 N N N 个城市&#xff0c;编号为 1 … N 1 \ldots N 1…N 小明是编号为 1 1 1 的城市中一家公司的员工&#xff0c;今天突然接到了上级通知需要去编号为 N N N 的城市出差。 由于疫情原因&#xff0c;很多直达的交…

【GEE实践应用】统计遥感数据像元的观测值数量以及良好观测值数量

下面我们以贵州省毕节市2016年8月1号至2018年7月31号两年间像元的观测值数量以及良好的观测值数量为例&#xff0c;统计结果以图像形式进行输出&#xff0c;如图1所示&#xff1a; // 1. 定义研究区域 var studyArea table;// 获取 Landsat 和 Sentinel-2 数据集 var landsat…

第九届少儿模特明星盛典 全球赛首席体验官『韩嘉滢』精彩回顾

2024年1月30日-2月1日&#xff0c;魔都上海迎来了龙年第一场“少儿形体行业美育春晚”&#xff01;由IPA模特委员会主办的第九届少儿模特明星盛典全球总决赛圆满收官&#xff01;近2000名少儿模特选手从五湖四海而来&#xff0c;决战寒假这场高水准&#xff0c;高人气&#xff…

前端上传照片压缩 (适合 vue vant组件的)

为什么要这样做&#xff1f; &#xff08;减小服务器压力 提升用户体验上传照片和加载照片会变快&#xff09; 最近有一个需求&#xff0c;通过手机拍照后上传图片到服务器&#xff0c;大家应该都知道&#xff0c;现在的手机像素实在是太高了&#xff0c;随便拍一张都是10M以上…

物联网的核心价值是什么?——青创智通

工业物联网解决方案-工业IOT-青创智通 物联网&#xff0c;这个词汇在当今的科技领域已经变得耳熟能详。但当我们深入探索物联网的核心价值时&#xff0c;我们会发现它远不止是一个简单的技术概念&#xff0c;而是一种能够彻底改变我们生活方式和工作方式的革命性力量。 物联网…

Django之rest_framework(三)

一、GenericAPIView的使用 rest_framework.generics.GenericAPIView 继承自APIVIew,主要增加了操作序列化器和数据库查询的方法,作用是为下面Mixin扩展类的执行提供方法支持。通常在使用时,可搭配一个或多个Mixin扩展类 1.1、属性 serializer_class 指明视图使用的序列化器…

JVM之JVM栈的详细解析

Java 栈 Java 虚拟机栈&#xff1a;Java Virtual Machine Stacks&#xff0c;每个线程运行时所需要的内存 每个方法被执行时&#xff0c;都会在虚拟机栈中创建一个栈帧 stack frame&#xff08;一个方法一个栈帧&#xff09; Java 虚拟机规范允许 Java 栈的大小是动态的或者是…

npm配置阿里镜像库

1、配置阿里云镜像源 #查看当前使用的镜像地址命令 npm config get registry#设置阿里镜像源 npm config set registry http://registry.npmmirror.com 这里要注意下&#xff0c;之前的镜像源地址 https://registry.npm.taobao.org/ 已经不能用了&#xff0c;这里要更改为新…

Grok-1.5 Vision 预览 将数字世界与物理世界连接起来,首款多模态模型

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

android 创建module

文章目的&#xff1a; 快速创建module并使用 创建步骤&#xff1a; 1 创建module 2 修改module下的build.gradle文件 3 修改清单文件中MainActivity属性&#xff0c;否则APP会因为有多个启动界面而崩溃 4 在主项目build.gradle引用该object Module 至此&#xff0c;可在APP中…

golang 迷宫回溯算法(递归)

// Author sunwenbo // 2024/4/14 20:13 package mainimport "fmt"// 编程一个函数&#xff0c;完成老鼠找出路 // myMap *[8][7]int 地图&#xff0c;保证是同一个地图&#xff0c;因此是引用类型 // i,j表示对地图的哪个点进行测试 func SetWay(myMap *[8][7]int, …

【ARM 裸机】汇编 led 驱动之烧写 bin 文件

1、烧写概念 bin 文件烧写到哪里呢&#xff1f;使用 STM32 的时候烧写到内部 FLASH&#xff0c;6ULL 没有内部 FLASH&#xff0c;是不是就不能烧写呢&#xff1f;不&#xff0c;6ULL 支持 SD卡、EMMC、NAND FLASH、NOR FLASH 等方式启动&#xff0c;在裸机学习的工程中&#x…

参会记录|全国多媒体取证暨第三届多媒体智能安全学术研讨会(MAS‘2024)

前言&#xff1a;2024年4月13日上午&#xff0c;我与实验室的诸位伙伴共聚江西南昌的玉泉岛大酒店&#xff0c;参加了为期一天半的全国多媒体取证暨第三届多媒体智能安全学术研讨会&#xff08;MAS’2024&#xff09;。本届学术研讨会由江西省计算机学会、江西省数字经济学会主…
最新文章