图解:HashMap 怎么聊出花来?

本文详细介绍了HashMap的工作原理,包括位桶+拉链法的实现,哈希冲突的解决(拉链法和红黑树),以及HashMap的优化策略如扩容和扰动函数。同时,讨论了HashMap在多线程环境下的线程安全性问题及解决方案。通过本文,你可以全面了解HashMap并应对面试中的相关问题。
摘要由CSDN通过智能技术生成

 

 

前言

 HashMap在面试的时候被问到的频率很多,读完这篇文章教你怎么暴打面试官。当然,最重要的还是要学习这些思想啦。我们一起来看看,hashmap有着什么隐藏的知识点吧。

 


什么是HashMap?

 HashMap是java中哈希表的实现。用于存储Key-Value键值对的集合,它通过把关键字值映射到一个位置来访问记录,以加快查找的速度。HashMap在java中底层是用了位桶法+拉链法去实现的。

不要被位桶法给吓到,其实说白了就是一个数组而已。我们可以举个例子去简单说明一下实现。

 场景:在hashmap中存入{1,3,5}

一个简单的实现:我们先假设这个位桶数组大小的4,哈希函数为(数字%数组长度)。那我们的映射关系就是通过哈希函数算出的key作为数组的下标存放。

插入“1“的情况

 

插入“3”的情况

 

插入”5“的情况

 

我们可以发现数组下标1的位置已经有数据了,放不下了怎么办?这其实是hash冲突的问题,hash冲突有多种解决方法,比如开发地址法,拉链法等。hashmap使用的是拉链法。

拉链法的好处:(1)链地址法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短。(2)由于链地址法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;(3)在用链地址法构造的散列表中,删除结点的操作易于实现。 

拉链法的缺点:(1)指针需要额外的空间,故当结点规模较小时就比较尴尬。 看看图噢

 


当拉链法导致的链表过长怎么?

当数据量多起来的时候,出现hash冲突过多,最坏的情况就是都挤在了一个位置上面,那本来查询很快的哈希表就会退化成在一个链表上查找的情况,时间复杂度为O(n),太内个了吧!】

 

解决方法一:java在JDK1.8的时候,当链表的长度超过8的时候,就会转化成红黑树。我先给出一张假假的红黑树图。(ps:原谅懒惰的作者吧)

红黑树是平衡二叉树的一种,增删改查的时间复杂度是O(logn)基本。如果是传统的平衡二叉树,为了保证平衡性(左右子树高度相差不超过一),左旋右旋的调整时间花销会比较大,所以就引入的红黑树,制定新的限制规则去保证平衡性,达到调整的时间开销与查找的时间开销可以达到一个好的平衡状态。

如果但数据量很大的时候,哪怕是引入了红黑树,查找时间是O(logn),也还是不能达到我们预期的一个效率。

解决方法二:当存入(hashmap的数据量 =  位桶数组的长度 * 加载因子 )的时候,会进行一个扩容操作。扩容的操作是这样的:(1)先开辟一个之前长度*2的长度位桶数组空间。(2)重新计算集合中的元素会放在新位置的那个桶中(%新长度)(3)删除原来的空间。

得知扩容操作的流程后,我们发现扩容是一个非常耗时的过程,因为它需要重新计算这些数据在新位桶数组中的位置并进行复制处理。敲黑板!!所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

 


HashMap的映射函数和哈希函数是怎么样的?

有读者看的这,很自信的说错是用(元素的val%位桶的长度)。哼,辉先森只能说太天真啦。在这之前我觉得有必要先说明下对于HashMap来说什么才是元素的val,是内容吗?那如果是这样的话,那string类型,对象类型怎么取模呢?首先我们需要注意的一点就是在java集合中可以存放的只能是对象类类型

你看到这可能会和我杠起来了啊,"java也有基本的数据类型啊,我也有将他们存放到hashmap里面啊"。啊对对对,那确实,但是本质还是存放对象类型。因为java将其知道转化为了包装类了。不清楚的读者可以先了解下什么是拆箱与装箱,这里咋们先不张开说明。

在java中Object类是所有类的祖先,java的object类函数有hashcode(),equals(),wait(),notify(),notifyAll(),finalize(),toString(),getClass(),clone()等函数。这会是一个考点哦。

看的这,我相信大家也可以猜到元素的val其实就是由hashcode()计算出来的。我们在来回归主题,HashMap的映射函数其实是hash & (length-1) ,这个算式是等价于hash % length,当然这是有一个前提就是length是2的次幂的时候成立。而hashmap初始的长度是16,每次扩容都*2,故满足前提条件length是2的次幂。

因为这样(length-1)正好相当于一个“低位掩码”。“与”操作的结果就是散列值的高位全部归零,只保留低位值,用来做数组下标访问。

但这时候问题就来了,这样就算我的散列值分布再松散,要是只取最后几位的话,碰撞也会很严重。更要命的是如果散列本身做得不好,分布上成等差数列的漏洞,恰好使最后几个低位呈现规律性重复,就太糟糕了一点。这个时候!就是这个时候,我们hashmap的哈希函数来了!他带着解决方法走来了!

 

这个函数很简单哦。右位移16位,正好是32bit的一半,自己的高半区和低半区做异或,就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性。而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来。这个我们也可以称之为扰乱函数。


为毛要用hash & (length-1)去代替 hash % length呢?

最主要的考虑就是效率。位运算(&)效率要比代替取模运算(%)高很多,主要原因是位运算直接对内存数据进行操作,不需要转成十进制,因此处理速度非常快。给个骚一点的解释: 位运算实现取模只需5个CPU周期,而取模运算符实现至少需要26个CPU周期。

所以就是出于运行的效率进行考虑的。


HashMap是线程安全的吗?

hashmap不是线程安全的,它设计的时候就没有考虑过线程安全的问题好吧。

hashmap线程不安全的主要原因是由于扩容rehash的时候。

transfer的过程:

首先获取新表的长度,之后遍历新表的每一个entry,然后每个ertry中的链表,以反转的形式,形成rehash之后的链表。

并发问题:

若当前线程此时获得ertry节点,但是被线程中断无法继续执行,此时线程二进入transfer函数,并把函数顺利执行,此时新表中的某个位置有了节点,之后线程一获得执行权继续执行,因为并发transfer,所以两者都是扩容的同一个链表,当线程一执行到e.next = new table[i] 的时候,由于线程二之前数据迁移的原因导致此时new table[i] 上就有ertry存在,所以线程一执行的时候,会将next节点,设置为自己,导致自己互相使用next引用对方,因此产生链表,导致死循环。

懵逼是吧,我们来看图!!

 

 

 

多线程的情况:

线程一关键流程:                                 线程二的关键流程:

1. e.next = newtable[i];                          1. e.next = newtable[i];

2. newtable[i]=e;                                    2. newtable[i]=e;

3. e=next;                                               3. e=next;

假如线程一在执行"e.next = newtable[i]; "中断了,而线程二正常的执行。执行的结果假如是下面的结果。

这时候线程一他好了,可以继续执行了。但是!!由于 newtable[i] , 已经被执行过newtable[i]=e了。所以它指向的就是桶里面的元素。本应该"a"的next 指向的应该是table[i]的地址,但是确指向了"b"的地址。

成环了

解决问题:

使用synchronize ,或者使用concurrentHashmap来解决。

 


总结

上面很多都是教会了大家hashmap做了什么优化,为什么,以及会出现什么问题。让我们总结一下当被问起HashMap时,我们该如何暴打面试官。

第一步:当我们听到,"你知道hashmap吗" 时,请保持微笑。例如:咧嘴战神!!! (面试官:???)

第二步:讲底层原理:位桶+拉链法 -> 拉链法的优缺点 -> 当数据量多时解决方法(jdk1.8红黑树+扩容) (面试官:好像有点东西哦)

第三步:讲为什么扩容的长度是2的次幂 -> 为什么用位运算替代模运算 -> hashmap为了均匀散列做的操作(hashmap的hash())(面试官:卧槽,牛牛牛)

第四步:讲hashmap在多线程下会怎么样,解决方案是什么。(面试官:败了败了)

当你完成了上面的步骤,面试官心里想到:好家伙,真有你的。

各位转载,记得贴出处哦。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值