java concurrentmap原理_详解Java7之前的HashMap与ConcurrentHashMap原理

一、HashMap结构图(数组+链表)

1d834ca8bb0e27d08686b9ed4815ddf8.png 1. entry是一个对象,里面存放key、value、hash值、next(下一个entry对象) 2. HashMap中的数组叫做table。size是指整个上述结构中,entry对象的数量

二、HashMap的put方法

HashMap可以说是JDK中一个的经典的轮子,其中有许多非常实用的方法,但是逻辑最复杂的,还是put(K key, V value)方法。

其中包括了找出一个比当前数大的2的次方数、计算key的hash值、数组扩容以及旧数组转移到新数组的一系列操作及问题。

下图是put方法的执行流程:

ab99e513f70fa2fdf48f5b9e8a3a46c9.png

三、put方法流程详解

1. 初始化数组:

当第一次放入entry元素,即数组为空时,会初始化数组。

初始化时,将构造方法传入的initCapacity进行计算,算出比initCapacity大的2的次方整数capacity(如7->8、15->16),并创建长度为capacity的数组。

计算比initCapacity大的2的次方整数capacity时,是用的Integer.highestOneBit()方法,这里就不细聊了。

2. key == null

因为HashMap允许key为null的情况存在,这种情况,我们会默认它的hash值为0,即放在数组的第一个下标中,即table[0]。

3. key != null

计算key的hash值:

08fb7f052c99c3ce7e0abfa3152e90e7.png

可以看到,求出key的hashcode之后,还对哈希值进行了很多次的右移运算,这样做的好处,我们之后再说。

计算key的hash值之后,会让该hash值与数组长度进行&运算,像这样: hash & table.length,得到的值为i,i就是这个key所在的数组下标,而hashcode的作用就是为了可以使数组的下标计算随机化。这样我们就能看出来上述的右移运算的作用了,可以使hash算法的散列化效果充分发挥,如果仅仅只是计算key的hashcode作为最终hash值,那么很有可能会发生数组占用效率低,而链表过长的情况。看到这里不得不感叹,JDK的工程师真的nb。

4.替换或创建entry

(1)找到相应的数组下标之后,首先判断该元素是否为null,如果为null,那么说明该位置还没有entry元素,直接放入就好。

(2) 如果不为null,说明有一个以上的entry元素组成的链表,则遍历该链表,对比key的值。

注意:这里要先对比key的hash值是否相等,如果不相等再用==和equals比较值,这样会提高比较的效率,因为hash值不同的两个对象,一定不相同。

如果key相同,则覆盖之前的value值。

如果整个链表没有相同的key,那么就新建一个entry元素,放入链表头,这就是大家一直说的头插法。

5. 扩容

其实这一步,是创建新的entry数组之前的逻辑。但是因为比较复杂,所以放在这里说明:

每次创建新的entry之前,都会先判断一下:(entry的数量是否达到了阈值)&&(当前新建entry的数组元素是否 != 空),满足条件之后,才会进行扩容;

扩容的过程就是把创建一个比原本长度大两倍的数组,因为数组长度改变,所以要把现有的entry链表分别根据hash值和新长度再计算新数组的下标,这个过程需要遍历整个HashMap,效率很低,所以,当我们在使用HashMap时,要尽量预估好需要存放的entry元素个数,尽量避免扩容。

四、HashMap在多线程下,扩容时的弊端

先看扩容时的代码段:

070d1aaa8bd1b3ee71833666d6c70fbd.png

翻译一下就是如果当前数组长度已经达到Integer对象所能表示的最大值时,将阈值也设置成Integer最大值(一般不会触发);否则新建一个数组,长度为现任数组的2倍,并将旧数组的元素转移到新数组。

转移数组:transfer方法

代码段:

d4b143e1f499364d38f0fdfc0a0228e8.png - 总结一下就是循环整个数组和链表,先计算出key的对应新数组的下标,再将e的next元素指向新数组的下标,最后把e移动到数组下标的元素上。 - 如果这时候,是多线程环境下进行扩容,因为e.next = newTable[i],所以很容易发生循环链表,详情可以去bilibili搜一下hashMap的底层原理,很多视频讲的都很细。 - 为了解决这种并发执行的扩容问题,JDK提供了ConcurrentHashMap。

五、并发扩容的解决方案

从根本上解决:使用HashMap时,估算好存放进Map的元素数量,这样就不会扩容,就不会引发扩容问题。

将整个HashMap加上锁,即JDK的HashTable。HashTable就是将整个HashMap的put和get方法,加上synchronized关键字,然后直接锁住整个方法,这样虽然安全,但是效率极低,笔者并没有在实际的生产环境中见到有人用HashTable。

只有在不确定HashMap元素数量且还涉及到并发场景的情况下,才推荐使用ConcurrentHashMap。

六、ConcurrentHashMap结构

结构图:

0548705256212c254abf109050f9415e.png 5. 可以看到,所谓的并发安全的ConcurrentHashMap,可以看作是若干个小的HashMap组成一个ConcurrentHashMap,每一个小的HashMap都被称为Segement。这样扩容时就不用锁住整个表,只需要锁住当前操作的Segement即可,可以提高执行效率。 6. 而每个Segement里有几个Entry,其实取决于初始化ConcurrentHashMap时,传入的一个concurrencyLevel参数——并发级别,根据这个参数进行Segement数组的length计算。而根据整个ConcurrentHashMap的数组长度/Segement数组的length得到每个Segement中的entry数组长度。 7. 线程采取自旋的方式获取锁,获取到segement之后,再进行put操作。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值