hashmap1.7和1.8区别&ConcurrentHashMap底层原理

大家都知道(jdk1.8)HashMap中计算数组下标是HashMap的核心算法。小编今天在看HashMap源码中看到了hash(Object key)方法百思不得其解。小编问百度,查找相关博客,甚至连HashMap的关于hash(Object key)英文解释都看了。但是都只是说了为了尽量均匀,没有详细讲。小编今天为大家详细讲解一下这两个问题。

HashMap中hash(Object key)的原理,为什么这样做?
先看下hash(Object key)方法,详细大家基本都能看懂,但是知道这一步(h = key.hashCode()) ^ (h >>> 16)原因的人很少。

static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
首先这个方法的返回值还是一个哈希值。为什么不直接返回key.hashCode()呢?还要与 (h >>> 16)异或。首先要了解以下知识点:

必备知识点.:^ 运算 >>>运算 &运算。

  1. h >>> 16 是什么,有什么用?
    h是hashcode。h >>> 16是用来取出h的高16,(>>>是无符号右移) 如下展示:

0000 0100 1011 0011 1101 1111 1110 0001

16

0000 0000 0000 0000 0000 0100 1011 0011
2. 为什么 h = key.hashCode()) 与 (h >>> 16) 异或
讲到这里还要看一个方法indexFor,在jdk1.7中有indexFor(int h, int length)方法。jdk1.8里没有,但原理没变。下面看下1.7源码

1.8中用tab[(n - 1) & hash]代替但原理一样。

static int indexFor(int h, int length) {
return h & (length-1);
}
这个方法返回值就是数组下标。我们平时用map大多数情况下map里面的数据不是很多。这里与(length-1)相&,

但由于绝大多数情况下length一般都小于2^16即小于65536。所以return h & (length-1);结果始终是h的低16位与(length-1)进行&运算。如下例子(hashcode为四字节)

例如1:为了方便验证,假设length为8。HashMap的默认初始容量为16

length = 8; (length-1) = 7;转换二进制为111;

假设一个key的 hashcode = 78897121 转换二进制:100101100111101111111100001,与(length-1)& 运算如下

0000 0100 1011 0011 1101 1111 1110 0001

&运算

0000 0000 0000 0000 0000 0000 0000 0111

= 0000 0000 0000 0000 0000 0000 0000 0001 (就是十进制1,所以下标为1)
上述运算实质是:001 与 111 & 运算。也就是哈希值的低三位与length与运算。如果让哈希值的低三位更加随机,那么&结果就更加随机,如何让哈希值的低三位更加随机,那么就是让其与高位异或。

补充知识:

当length=8时 下标运算结果取决于哈希值的低三位

当length=16时 下标运算结果取决于哈希值的低四位

当length=32时 下标运算结果取决于哈希值的低五位

当length=2的N次方, 下标运算结果取决于哈希值的低N位。

  1. 原因总结
    由于和(length-1)运算,length 绝大多数情况小于2的16次方。所以始终是hashcode 的低16位(甚至更低)参与运算。要是高16位也参与运算,会让得到的下标更加散列。

所以这样高16位是用不到的,如何让高16也参与运算呢。所以才有hash(Object key)方法。让他的hashCode()和自己的高16位^运算。所以(h >>> 16)得到他的高16位与hashCode()进行^运算。

  1. 为什么用^而不用&和|
    因为&和|都会使得结果偏向0或者1 ,并不是均匀的概念,所以用^。

这就是为什么有hash(Object key)的原因。

1.7

put null 值 在0号位

在这里插入图片描述

便利节点修改相同属性hash和 key进行修改
在这里插入图片描述

进行覆盖
判断是否需要扩容
头插法
扩容创建新的数组讲put的位置进行对称转移
hash % 长度为table位置

1.8

JDK8中HashMap的put方法的实现过程?


1. 根据key生成hashcode
2. 判断当前HashMap对象中的数组是否为空,如果为空则初始化该数组
3. 根据逻辑与运算,算出hashcode基于当前数组对应的数组下标i
4. 判断数组的第i个位置的元素(tab[i])是否为空
a. 如果为空,则将key,value封装为Node对象赋值给tab[i]
b. 如果不为空:
ⅰ. 如果put方法传入进来的key等于tab[i].key,那么证明存在相同的key
ⅱ. 如果不等于tab[i].key,则:
1. 如果tab[i]的类型是TreeNode,则表示数组的第i位置上是一颗红黑树,那么将key和value插入到红黑树中,并且在插入之前会判断在红黑树中是否存在相同的key
2. 如果tab[i]的类型不是TreeNode,则表示数组的第i位置上是一个链表,那么遍历链表寻找是否存在相同的key,并且在遍历的过程中会对链表中的结点数进行计数,当遍历到最后一个结点时,会将key,value封装为Node插入到链表的尾部,同时判断在插入新结点之前的链表结点个数是不是大于等于8,如果是,则将链表改为红黑树。
ⅲ. 如果上述步骤中发现存在相同的key,则根据onlyIfAbsent标记来判断是否需要更新value值,然后返回oldValue
5. modCount++
6. HashMap的元素个数size加1
7. 如果size大于扩容的阈值,则进行扩容

JDK8中HashMap的get方法的实现过程

1. 根据key生成hashcode
2. 如果数组为空,则直接返回空
3. 如果数组不为空,则利用hashcode和数组长度通过逻辑与操作算出key所对应的数组下标i
4. 如果数组的第i个位置上没有元素,则直接返回空
5. 如果数组的第1个位上的元素的key等于get方法所传进来的key,则返回该元素,并获取该元素的value
6. 如果不等于则判断该元素还有没有下一个元素,如果没有,返回空
7. 如果有则判断该元素的类型是链表结点还是红黑树结点
a. 如果是链表则遍历链表
b. 如果是红黑树则遍历红黑树
8. 找到即返回元素,没找到的则返回空

ConcurrentHashMap

JDK7中的ConcurrentHashMap是怎么保证并发安全的?

主要利用Unsafe操作+ReentrantLock+分段思想。
主要使用了Unsafe操作中的:
1. compareAndSwapObject:通过cas的方式修改对象的属性
2. putOrderedObject:并发安全的给数组的某个位置赋值
3. getObjectVolatile:并发安全的获取数组某个位置的元素
分段思想是为了提高ConcurrentHashMap的并发量,分段数越高则支持的最大并发量越高,程序员可以通过concurrencyLevel参数来指定并发量。ConcurrentHashMap的内部类Segment就是用来表示某一个段的。
每个Segment就是一个小型的HashMap的,当调用ConcurrentHashMap的put方法是,最终会调用到Segment的put方法,而Segment类继承了ReentrantLock,所以Segment自带可重入锁,当调用到Segment的put方法时,会先利用可重入锁加锁,加锁成功后再将待插入的key,value插入到小型HashMap中,插入完成后解锁。

JDK7中的ConcurrentHashMap的底层原理

ConcurrentHashMap底层是由两层嵌套数组来实现的:

1. ConcurrentHashMap对象中有一个属性segments,类型为Segment[];
2. Segment对象中有一个属性table,类型为HashEntry[];
当调用ConcurrentHashMap的put方法时,先根据key计算出对应的Segment[]的数组下标j,确定好当前key,value应该插入到哪个Segment对象中,如果segments[j]为空,则利用自旋锁的方式在j位置生成一个Segment对象。
然后调用Segment对象的put方法。
Segment对象的put方法会先加锁,然后也根据key计算出对应的HashEntry[]的数组下标i,然后将key,value封装为HashEntry对象放入该位置,此过程和JDK7的HashMap的put方法一样,然后解锁。
在加锁的过程中逻辑比较复杂,先通过自旋加锁,如果超过一定次数就会直接阻塞等等加锁。
JDK8中的ConcurrentHashMap是怎么保证并发安全的?
主要利用Unsafe操作+synchronized关键字。
Unsafe操作的使用仍然和JDK7中的类似,主要负责并发安全的修改对象的属性或数组某个位置的值。
synchronized主要负责在需要操作某个位置时进行加锁(该位置不为空),比如向某个位置的链表进行插入结点,向某个位置的红黑树插入结点。
JDK8中其实仍然有分段锁的思想,只不过JDK7中段数是可以控制的,而JDK8中是数组的每一个位置都有一把锁。
当向ConcurrentHashMap中put一个key,value时,
1. 首先根据key计算对应的数组下标i,如果该位置没有元素,则通过自旋的方法去向该位置赋值。
2. 如果该位置有元素,则synchronized会加锁
3. 加锁成功之后,在判断该元素的类型
a. 如果是链表节点则进行添加节点到链表中
b. 如果是红黑树则添加节点到红黑树
4. 添加成功后,判断是否需要进行树化
5. addCount,这个方法的意思是ConcurrentHashMap的元素个数加1,但是这个操作也是需要并发安全的,并且元素个数加1成功后,会继续判断是否要进行扩容,如果需要,则会进行扩容,所以这个方法很重要。
6. 同时一个线程在put时如果发现当前ConcurrentHashMap正在进行扩容则会去帮助扩容。

JDK7和JDK8中的ConcurrentHashMap的不同点

这两个的不同点太多了...,既包括了HashMap中的不同点,也有其他不同点,比如:
1. JDK8中没有分段锁了,而是使用synchronized来进行控制
2. JDK8中的扩容性能更高,支持多线程同时扩容,实际上JDK7中也支持多线程扩容,因为JDK7中的扩容是针对每个Segment的,所以也可能多线程扩容,但是性能没有JDK8高,因为JDK8中对于任意一个线程都可以去帮助扩容
3. JDK8中的元素个数统计的实现也不一样了,JDK8中增加了CounterCell来帮助计数,而JDK7中没有,JDK7中是put的时候每个Segment内部计数,统计的时候是遍历每个Segment对象加锁统计

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值