day07-HashMap原理-1.7死锁

19 篇文章 0 订阅

Hashmap底层数据结构

jdk1.7之前,Hashmap底层数据结构是数组+链表实现的,1.8之后采用了红黑树对Hashmap进行了优化。

数组

使用一-段连续存储单元存储数据。对于指定下标的查找,时间复杂度为O(1),对于一般的插入删除操作, 涉及到数组元素的移动,其平均复杂度为O(n)

链表

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。

红黑树(>jdk7)

红黑树(Red Black Tree) 是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组。
红黑树是在1972年由Rudolf Bayer发明的,当时被称为平衡二叉B树(symmetric binary B-trees)。后来,在1978年被 Leo J. Guibas 和 Robert Sedgewick 修改为如今的“红黑树”
红黑树是一种特化的AVL树(平衡二叉树),都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。
它虽然是复杂的,但它的最坏情况运行时间也是非常良好的,并且在实践中是高效的: 它可以在O(log n)时间内做查找,插入和删除,这里的n 是树中元素的数目。

HashMap原理

先明白几个变量

初始容量
initialCapacity 必须要是2的指数次幂,保证HashMap在扩容时安全需求和性能需求
加载因子
loadFactor = 0. 75(默认是0.75)
threshold = initialCapacity * loadFactor 扩容阀值

jdk1.7之前,Hashmap底层数据结构是数组+链表实现的,这两种数据结构是怎么配合使用的呢?如下图所示。
在这里插入图片描述

初始化数据结构

这是一个构造函数,可以看出数组的构建不是在构造方法中的,构造方法只是判断初识容量、和加载因子是否合法,非法就抛异常,最后的init()方法是一个空方法。
在这里插入图片描述

  • 在第一次调用put()方法时,会判断table是否初始化,第一次调用肯定是没有的,只是对一个成员属性进行了赋值,在inflateTable()方法中会进行一个数据结构的初始化。
    在这里插入图片描述
    注释中说明:Find a power of 2>=toSize ,如果给的初始容量,在范围内但是不是2的幂次方,在做初始化时roundUpToPowerOf2()会进行更改扩容阈值。这里也可以看出table就是一个数组。
    在这里插入图片描述
    在这里插入图片描述

添加元素

  • hash()根据key的hashCode计算hash值,通过hash值跟数组的长度去进行计算,这个元素应该放在哪个索引位置。
    在这里插入图片描述
    这里为什么不直接使用hashCode()得到的值作为hash值呢?因为hashCode出现hash碰撞的几率很大,这样会使得链表长度增长,查询效率变低。所以这里通过“位扰动”(就是一系列的位运算等)的方式让hash散列,减少hash碰撞。返回值是一个int。

  • indexFor():通过hash值计算出在table数组的具体索引位置,这里采用的是hash值和table数组长度减一求与运算的方式。
    为什么不是直接用hash值%16取模呢?因为首先效率没有二进制运算效率高(其实取模,计算机还是把数组转换为二进制去计算的),散列度也不高(散列度要求高是为了让数组的每个索引位置(坑位)都能均匀的分布元素)还有为什么要length-1。

因为数组下标定位:h & length-1  (数组初识长度为16)  h就是计算出来的hash值
length不减1:
	h           1010 1111                 0001 0101 	  ……
	length      0001 0000                 0001 0000 	  ……
	结果:       0000 0000                 0001 0000		  ……
	不减一时发现结果要么是0要么是16,散列度差,而16时还会下标越界异常
length-1的情况:
	h           1010 1111               0001 0101  	……
	length-1    0000 1111               0000 1111  	……
	结果:       0000 1111               0000 0101  	……
减一后计算结果就在0~15之间,这也就是为什么要求要容量必须是2的指数次幂的原因,这样就能刚好产生 0~N (每一位)的位置索引了。

在这里插入图片描述

  • 找到对应索引下标时,会遍历该下标下的链表,如果该下标下的链表不为空,说明之前就发生过hash碰撞,就接着去判断:要插入元素的hash和当前遍历元素的hahs是否相等,且要插入元素的key和当前遍历元素的key是否相等,如果相等,那么就更新旧值为新值。
    在这里插入图片描述
  • 如果该下标下还没有链表存在,也就是这个待插入元素将是该下标的第一个元素,这就会去添加元素了,此时会调用addEntry()添加。
    这里可能会触发扩容机制,size>扩容阈值并且待插入位置也有元素,才能进行扩容,两个条件缺-一个都不会去扩容
    在这里插入图片描述
  • createEntry():创建新的节点,并且采用头插法将该元素插入到改下标下的链表中。这里的bucketIndex就是该下表值,先是吧该下标的链表首元素记录在e中,然后在修改他赋上新元素。添加元素成功。
    在这里插入图片描述
    Entry结构如下:
    在这里插入图片描述

数组扩容

addEntry()方法中,size>扩容阈值并且待插入位置也有元素,才能进行扩容,两个条件缺-一个都不会去扩容两个条件满足时,会触发扩容机制。
在这里插入图片描述

  • 这里可以看出调用resize(2*table.length),说明每次扩容默认是扩为原来数组长度的2倍,这个倍数也同样满足了2的指数幂这个条件。
    在这里插入图片描述
  • initHashSeenAsNeeded():返回一个Boolean值作为transfer方法的参数传递,是判断是否要进行重新hash的依据。
    在这里插入图片描述
  • transfer():扩容后的新数组,保证元素不丢失,肯定要把原来数组的元素一一拷贝到新数组中,transfer方法就是做这个事情的。而死锁也是在这里产生的。该方法执行完后,元素位置倒置,因为在复制到新数组的时候,对应位置的链表复制也是采用头插法,而且是从下标位置开始遍历的(那么这个开始位置复制完成就变成尾了),类似于先进后出的道理一样,原来的头复制到新数组后会变成尾,而原来的尾复制到新数组后反而变成了头,就出现链表元素倒置。如果多线程下很有可能会让链表成环,出现死循环,即死锁现象。
    代码中int i=indexFor(e.hash,newCapacity)会根据扩容后新的容量和要复制的元素的hash值再次求对应的下标位置。

在这里插入图片描述

原理图

在这里插入图片描述

源码执行图

在这里插入图片描述
jdk1.8优化了这个问题,但是官方建议在并发情况下还是不使用HashMap,因为可能导致元素丢失的情况,建议使用ConcurrentHashMap,他的底层实现和HashMap一样,只是引入了一些原子操作。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值