HashMap常见问题

HashMap学习

1基本数据结构和行为特征

hashmap的底层数据结构是由数组、链表以及红黑树组成。默认初始化时的大小为16,内部存在负载因子参数默认0.75。每当hashmap的容量达到当前容量*0.75时,就会引发hashmap的扩容,每次扩容就是向左移一位,也就是2倍,而容量设置需要设置为2的倍数。

2如何进行hash取值

说到对hasmmap如何取值,相信就算是刚入门的小白也可能清楚,那就是对hashmap的容量进行取模获取到对应的位置。但是到底如何取模呢?直接拿到值进行取模操作吗?可能真相会与你的猜想大相径庭。

让我们直接上源码,毕竟源码无秘密

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

使用hashmap首先得调用put方法,将key和value传入,再对key取其hashcode,并且与该hashcode右移16位后的值进行异或操作。(异或操作:相同为0,不同为1)

那么问题来了,为什么要做这样的操作呢?不急,我们先接着往下看

在putVal()方法中,截取一段代码取模操作的代码

if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);

(n - 1) & hash,将容量长度减1再对hash值进行与操作。

1、首先hash值为什么要与右移16位后的值进行异或操作?

这是因为大多数的hashmap的容量都会不超过16位,如果直接用32位的hash值进行与操作,那么高16位的值根本就没有机会进行与操作。那么这么一个操作,将高16位和低16位的特征都融入了低16位中,这样hash碰撞的概率就会变小。

2、为什么做与操作而不是直接取模?

在计算机中,位运算的速度是远远大于取模运算的,而使用与操作就能实现取模的效果,那当然是用位运算来实现。

3、为什么hashmap的容量需要设置为2的倍数

就是由于使用(n - 1)& hash的位运算来代替了取余运算,而2的倍数的二进制肯定是最高位为1余位全为0,那么在n - 1后就会变成最高位为0,余位全为1。这样的数字来进行与运算才能减少hash冲突,所以需要设置为2的倍数

举个例子:

假设当前hashmap的容量为9,现在要2和3确定位置

​ 9的二进制:0000 1001 (n - 1):0000 1000

2的二进制:0000 0010 位运算:(0000 1000)& (0000 0010)= 0

3的二进制:0000 0011 位运算:(0000 1000)& (0000 0011)= 0

产生hash冲突

 

那么如果容量为8呢?

8的二进制:0000 1000 (n - 1):0000 0111

2的二进制:0000 0010 位运算:(0000 0111)& (0000 0010)= 2

3的二进制:0000 0011 位运算:(0000 0111)& (0000 0011)= 3

没有hash冲突

3怎么解决hash碰撞

不管怎么去设计,对于散列表来说一个回避不了的问题,那就是碰撞的问题。什么是hash碰撞,当两个不同的key进行hash操作之后定位到的位置是同一个位置,这个就是hash碰撞。

在1.7的版本里只是简单的使用拉链法来解决hash碰撞,当碰撞的数据变多的时候,查询该key的时间复杂度就变成了O(n),因为不得不去遍历这个链表来获取到对应的数据(这里获取数据的首先进行hashcode的取值进行位置定位,定位之后如果发现是个链表,那么就会使用equals来进行判断,这也是为什么hashcode与equals需要一起进行重写的原因)

而在1.8的版本中除了使用拉链法之外还引入了红黑树的算法,当链表数据达到8的时候,链表转换为红黑树,红黑树它就能给让数据保持有序且平衡的状态,那么树的查询那就好办了,时间复杂度仅为O(log n)。

4并发场景resize产生环形链表导致CPU100%?

在1.7hashmap中采用头插法导致在并发场景下会形成环形列表的bug。但是我觉得这个问题就算是不解决也没什么问题,因为hashmap本来就不是在并发场景下使用的容器,当然JDK为了代码的健壮性在1.8的版本里采用尾插法解决了这个问题。

resize方法的核心就是用两重循环来重建hashmap,第一次循环会拿到槽位上的元素,然后do while遍历会槽位上的所有元素,元素会按照顺序重新加到新的hashmap的槽位上​。

那么假设某个槽位上有三个元素,分别为(k1,v1)->(k2,v2)->(k3,v3)那么,重建过程中可能新hashmap的某个槽位又会正好对应这三个元素,那么元素的该槽位对应的变化就是:

1、(k1,v1)

2、(k2,v2) ->(k1,v1)

3、(k3,v3)->(k2,v2)->(k1,v1)

最终会把链表的顺序对调。

然后另一个线程来了,此时它首先拿到(k1,v1),这步的时候还不会有问题。然后拿到(k2,v2),将槽位指向(k2,v2),这时继续进行链表遍历,发现(k2,v2)的下一个节点是(k1,v1),那么就会将槽位指向(k1,v1),并将(k1,v1)指向(k2,v2)

 

 

这样环形链表就形成了

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值