HashMap

HashMap是用于存储键值对 Key Value的集合,每一个键值对也叫做Entry.这些键值对也分散存储在一个数组当中,这个数组就是HashMap的中心
HashMap数组每一个元素的初始值都是null;

1.对于Put的原理:
比如我们插入一个键值对的时候,Key就为apple Value为:0.这时候我们需要利用一个哈希函数来确认Entry插入的位置(Index):
index = Hash(“apple”)
哈希函数算法与key进行算法操作
假如得到的index为:2
在这里插入图片描述
但是HashMap的长度是有限的,所以在插入大量的Entry是,在难免可以会出现index冲突的问题
在这里插入图片描述
那么现在该如何解决?
现在链表就登场了
HashMap数组每一个元素不止是一个Entry对象,也是一个链表的头节点,每个插入完成的Entry对象都有Next指针 指向它的下一个Entry节点,当新来的Entry映射到冲突的数组位置时,只需要插入到指定的链表节点里即可。
在这里插入图片描述
2.Get原理:
在使用get方法根据key,来查找Value时发生了什么变化?
首先也是一样 根据key跟哈希函数 做映射,获取到对应的Index
index = Hash(“apple”)
由于可能出现Hash冲突,同一个位置可能有匹配到多个Entry,这个时候就需要顺着对应的链表的头节点,一个一个向下查找。假设我们查找的key是apple
在这里插入图片描述
1.我们查看的是头节点Entry6,但是它的key不是apple,所以不是我们想要的结果
2.那么接着看Next节点的指向,Entry1,它的key是apple,那么它就是我们所需要的结果.

之所以把Entry6放在头节点,是因为HashMap的发明者认为,后插入的Entry查找性可能比较大.

要注意的是HashMap的默认长度是16,每次进行扩展长度必须是2的幂,还有指所以选择16是因为服务于从key映射到index的Hash算法
采用的是 位运算的方式
index = HashCode(Key)&(Length(长度)-1)

下面我们以值为“book”的Key来演示整个过程:

1.计算book的hashcode,结果为十进制的3029737,二进制的101110001110101110 1001。

2.假定HashMap长度是默认的16,计算Length-1的结果为十进制的15,二进制的1111。

3.把以上两个结果做与运算,101110001110101110 1001 & 1111 = 1001,十进制是9,所以 index=9。

可以说,Hash算法最终得到的index结果,完全取决于Key的Hashcode值的最后几位。

HshMap的容量也是有限的:
当多次插入Entry时,会使得HashMap达到一定的饱和度时,Key映射位置发生冲突的几率也大大提高
这个时候,HashMap就需要自动扩展它的长度了,也就是进行Resize.
那么影响发生Resize的因素有两个:
1.Capacity
HashMap的当前长度,是2的幂
2.LoadFactor
HashMap的负载因子,默认值为0.75f
衡量HashMap是否进行Resize的条件就是:

  • HashMap.size >= Capacity * LoadFactor

当HashMap触发Resize时扩展需要经过以下2个步骤

  • 1.扩容:创建一个新的Entry空数组,但是长度是原来的2倍
  • 2.ReHash:遍历议原Entry数组。把所有的Entry重新Hash到新的数组,为什么要重新Hash呢?因为它的长度变了,所以Hash的运算规则也要重新改变

回顾以下Hash公式:
index = HashCode(key) & (Length-1)

当原数组长度为8时,Hash运算是和111B做运算,
那么新数组长度为16时,Hash运算是和1111B做运算,结果肯定不同

Resize前的HashMap:
在这里插入图片描述
Resize后的HashMap:
在这里插入图片描述

ReHash的Java代码如下:
在这里插入图片描述
但是HashMap在多线程的情况下是不安全的:
假设一个HashMap已经到了Resize的临界点。此时有两个线程A和B,在同一时刻对HashMap进行Put操作:
在这里插入图片描述
在这里插入图片描述

此时达到Resize条件,两个线程各自进行Rezie的第一步,也就是扩容:
在这里插入图片描述

这时候,两个线程都走到了ReHash的步骤。让我们回顾一下ReHash的代码:

在这里插入图片描述

假如此时线程B遍历到Entry3对象,刚执行完红框里的这行代码,线程就被挂起。对于线程B来说:

e = Entry3
next = Entry2

这时候线程A畅通无阻地进行着Rehash,当ReHash完成后,结果如下(图中的e和next,代表线程B的两个引用):
在这里插入图片描述

直到这一步,看起来没什么毛病。接下来线程B恢复,继续执行属于它自己的ReHash。线程B刚才的状态是:

e = Entry3
next = Entry2
在这里插入图片描述

当执行到上面这一行时,显然 i = 3,因为刚才线程A对于Entry3的hash结果也是3。

在这里插入图片描述

我们继续执行到这两行,Entry3放入了线程B的数组下标为3的位置,并且e指向了Entry2。此时e和next的指向如下:

e = Entry2
next = Entry2

整体情况如图所示:

在这里插入图片描述

接着是新一轮循环,又执行到红框内的代码行:

在这里插入图片描述

e = Entry2
next = Entry3

整体情况如图所示:

在这里插入图片描述

接下来执行下面的三行,用头插法把Entry2插入到了线程B的数组的头结点:

在这里插入图片描述

整体情况如图所示:

在这里插入图片描述

第三次循环开始,又执行到红框的代码:

在这里插入图片描述

e = Entry3
next = Entry3.next = null

最后一步,当我们执行下面这一行的时候,见证奇迹的时刻来临了:
在这里插入图片描述

newTable[i] = Entry2
e = Entry3
Entry2.next = Entry3
Entry3.next = Entry2

链表出现了环形!

整体情况如图所示:
在这里插入图片描述
此时问题还没有产生,当我们调用get查询一个不存在的key时,而这个key的Hash结果恰好等于3的时候,那么由于3的位置时环形链表,所以程序就进入了死循环!

所以说在高并发的情况下,我们通常采用另外一个集合类:ConcurrentHashMap

注意:
1.HashMap在插入元素过多时需要进行Resize,那么它的触发条件时?
HashMap.size >= Capacity * LoadFactor
2.HashMap的Resize包含扩容和ReHash两个步骤,ReHash在高并发的情况下可能形成环形链表

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值