java基础(二)HashMap相关问题,HashMap 1.7和HashMap 1.8的区别,ConcurrentHashMap1.7和1.8的不同实现

1、时间复杂度

常见的时间复杂度排序:
Ο(1)<Ο(log2n)<Ο(n)<Ο(nlog2n)<Ο(n2)<Ο(n3)<…<Ο(2n)<Ο(n!)

2、HashMap的时间复杂度

由于HashMap底层是通过 数组加链表 的方式实现的,要找到数组对应的下标,只需要进行一次取模运算,但是找到数组对应下标之后,如果entry链表太长的话,还是需要时间遍历比较。所以说entry链表越短越好,最好每个数组后面只跟一个链表,这样就能达到 O(1) 的时间复杂度,
如果最差的情况下,即所有元素的hashCode都一样,那就放在同一个bucket后面,这样时间复杂度就会达到 O(n),但这种情况概率几乎没有。

3、为什么采用哈希码 与(&) (数组长度-1) 计算数组下标?

数组长度要求为2的幂原因:

  • (1) 数组长度 = 2的幂,(二进制)表示为100…00,首位为1,后面位数均为0;
           (数组长度-1)的结果 = (二进制)表示为0111.111,首位为0,后面位数均为1.

  • (2)提高运算效率。
             哈希码“与运算” 实绩长度 = hash值 对数组长度取模,但是取余效率较低,
             只有当数组长度为2的幂的时候,h&(length-1) 才等价于 h%length

  • (3)保证哈希码的均匀性。
            数组长度为2的幂 = 偶数(二进制最后一位为0),(length-1)为奇数(二进制最后一位为1),使得哈希码 & (数组长度-1)的结果最后一位可能是0或者1(取决于哈希码),即求与运算结果可能是奇/偶,保证了存储位置的均匀分布性。

    eg:如果length不是2的次幂,比如length等于15,那么length-1=14,对应二进制为1110,与哈希码进行“与”操作最后一位永远都是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素,空间浪费很大,而且这种情况中,数组可以使用的长度比数组的实际长度小了很多,这也意味着增加了哈希碰撞的几率。

4、HashMap为什么不安全?

(1)多线程put的时候导致数据不一致
        比如有两个线程A和B,A在向HashMap中put数据的时候,首先计算出数组的下标,然后获取该桶的链表表头节点,此时线程A的时间片用完了,而此时线程B得以调用。
线程B成功的插入了数据。假设线程A和线程B的hashCode值计算出来的桶索引是一样的,当线程B成功插入后,线程A再度调用执行,线程A持有过期的链表头而不知道,
导致线程A直接覆盖了之前线程B插入的数据,造成数据不一致的结果。

(2)当两个线程同时执行扩容的时候,容易线程不安全

死循环现象

一般来说,当有数据插入时,会先检查是否需要扩容,如果超过需要进行扩容,重新计算hash值

转移表的过程transfer的操作步骤:

  • 1、 对索引数组中的元素遍历
  • 2、对链表上的每一个节点进行遍历,将节点e转移到新表中采用头插法,用next取得要转移的下一个元素
  • 3、循环2,直到所有数据都被转移
  • 4、循环1,直到所有数组都被转移

经过这几步,就会发现转移的时候是逆序的。假如转移之前的顺序是1->2->3,转移完之后就是3->2->1。这就有可能发生死锁,1->2,2->1,所以HashMap的死锁问题就出现在transfer这个方法上。但是JDK1.8中新老数组已经不存在引用关系了,因此不会出现死循环现象,但是还有可能会存在键值对丢失的情况。

键值对丢失

5、HashMap 1.7和HashMap 1.8的区别

5.1
1.8的存储结构为数组+链表+红黑树,1.7的为数组+链表。

引入红黑树原因:
提高HashMap性能,解决了由于哈希碰撞链表过长导致索引效率慢的问题。利用红黑树增删改查的特点,时间复杂度从O(n)降低为O(logn)

1.8中HashMap中的数组元素 & 链表节点 采用 Node类 实现,HashMap中的红黑树节点 采用 TreeNode 类 实现
在这里插入图片描述
5.2
添加红黑树之后,添加的核心参数。
(1).桶的树化阈值
即链表转成红黑树的阈值,在存储数据时,当链表长度大于该值时,则将链表转换成红黑树。
(2).桶的链表还原阈值
-即红黑树转换为链表的阈值。
-当在扩容之后,HashMap的存储位置会重新计算,此时如果原有的红黑树内数量 小于 阈值时,将红黑树转换为链表
(3). 最小树形化容量阈值
-当哈希表中的容量 > 该值时,才允许树形化链表。
-否则,若桶内元素过多,则直接扩容,而不是树形化。
-为了避免扩容和树形化的冲突,这个值不能小于 4 * 桶的树形化阈值

5.3.插入数据方式不同:
JDK1.8采用尾插法,JDK1.7采用的是头插法。
在这里插入图片描述
5.4.扩容之后存储位置的计算方式:
(1)按照扩容后的规律计算:扩容后的位置=原位置 or 原位置+旧容量

第一种理解:
因为插入值的hashcode是不会变的,所有newIndex只与第五位bit有关
如果与第五位bit进行&运算的结果为0,那么newIndex= oldIndex,下标不变;
如果&运算为1,那么newIndex = oldIndex + oldCapacity

第二种理解:
可以直接理解为用插入值的hashcode 和 oldCapacity.lenth 进行&操作,如果结果为0,那么位置不变,newIndex = oldIndex。
如果结果为1,那么newIndex = oldIndex + oldCapacity

在这里插入图片描述

(2)1.7全部按照原来的计算方式计算,hashCode()->>扰动处理->>(h&(length-1))
在这里插入图片描述
图片转载自:
区别详解:https://blog.csdn.net/carson_ho/article/details/79373026
JDK1.8详解:https://blog.csdn.net/carson_ho/article/details/79373134

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值