HashMap解决死锁、ConcurrentHashMap原理

对于HashMap大家应该都不陌生,对于hashmap扩容以及hashmap存储大家肯定也都知道,官方说明在达到负载因子0.75的时候就会促发扩容机制,那么问题就来了,为什么HashMap的默认负载因子是0.75,而不是0.5或者是整数1呢?

阈值(threshold) = 负载因子(loadFactor) x 容量(capacity)

HashMap扩容原理

写数据之后会可能触发扩容,HashMap结构内,有一个记录当前数据量的字段,这个数据量字段到达扩容阈值的话,它就会触发扩容的操作

阈值(threshold) = 负载因子(loadFactor) x 容量(capacity)

当HashMap中table数组(也称为桶)长度 >= 阈值(threshold) 就会自动进行扩容。

扩容的规则是这样的,因为table数组长度必须是2的次方数,扩容其实每次都是按照上一次tableSize位运算得到的就是做一次左移1位运算,

假设当前tableSize是16的话 16转为二进制再向左移一位就得到了32 即 16

容量的两倍,但记住HashMap的扩容是采用当前容量向左位移一位(newtableSize = tableSize

怎么说也就可以解答为什么是0.75了,现在有两种说法:

一种是:

根据HashMap的扩容机制,他会保证容量(capacity)的值永远都是2的幂 采用位运算 让hash更加散列, 为了减少hash碰撞,

为了保证负载因子x容量的结果是一个整数,这个值是0.75(3/4)比较合理,因为这个数和任何2的次幂乘积结果都是整数。

理论上来讲,负载因子越大,导致哈希冲突的概率也就越大,负载因子越小,费的空间也就越大,这是一个无法避免的利弊关系,所以通过一个简单的数学推理,可以测算出这个数值在0.75左右是比较合理的

另一种说法:

当负载因子为1.0时,意味着只有当hashMap装满之后才会进行扩容,虽然空间利用率有大的提升,但是这就会导致大量的hash冲突,使得查询效率变低。当负载因子为0.5或者更低的时候,hash冲突降低,查询效率提高,但是由于负载因子太低,导致原来只需要1M的空间存储信息,现在用了2M的空间。最终结果就是空间利用率太低。(负载因子是0.75的时候,这是时间和空间的权衡,空间利用率比较高,而且避免了相当多的Hash冲突,使得底层的链表或者是红黑树的高度也比较低,提升了空间效率 )

底层为什么使用链表:为了解决hash冲突

  • HashMap数组元素为链表的时候,插入直接使用头插,插入复杂度O(1);当链表较短时候,查找数据时对性能并没有什么影响,如果链表一长,查找起来就很影响性能了。
  • 链表:插入复杂度O(1),查找复杂度O(n)
  • 红黑树:插入复杂度O(logn),查找复杂度O(logn)

HashMap数组元素为链表的时候,插入直接使用头插,插入复杂度O(1);当链表较短时候,查找数据时对性能并没有什么影响,如果链表一长,查找起来就很影响性能了。

在Java8中,如果链表长度到达了8个,就会转化为红黑树,提高了查找的性能,但每次插入新的数据,都得维护红黑树的结构,复杂度为O(logn)。这样算是对查找和插入元素时性能的一个权衡,毕竟存起来就是用来查的

hashMap使用数组 当put值计算hashCode时候找到数组对应位置插入复杂度O(1);

HashMap死锁的问题

基础数据模型:数组加链表 一开始threshold扩容阈值默认 16*0.75 当数组容量达到一定程度就会扩容 数组默认长度为16 阈值(threshold) = 负载因子(loadFactor) x 容量(capacity) 得到扩容临界值 如果达到临界值就会去 对数组进行扩容 需要将原来的数组内容 移动到新的数组里面去 此时可能会产生链表的闭环导致死循环。

死锁的原因:在多线程场景下扩容期间,由于存在指针节点位置互换 指针引用的问题,有可能导致链表闭环。

也就是在HashMap在put方法时候会调addEntry方法 判断当前是否应该扩容 在addEntry()方法里面有一个resize()扩容方法当判断数组长度超过阀值得时候就会执行这个方法 resize()扩容 主要产生死锁的原因是resize()方法中的transfer()方法里面 链表会形成闭环导致死循环

resize()扩容过程: 当数组进行扩容 在将原来数据移动到新数组里面得时候 将通过next指针进行数据的移动(往新的数组中移动的时候),这个移动先改变原有链表的顺序 通过next指针再恢复成之前的数据 再next指针引用的时候可能会导致链表的闭环成死循环。

由于hashmap线程不安全======>可能产生数据丢失 原因当两个线程同时去put值得时候 产生hash冲突 此时如果没有引起扩容 一个线程就可能将之前线程put的值覆盖,而导致当去get得时候为null。

在JDK1.8解决死锁问题

使用两组指针高低位搭配,不会改变原有数据顺序 所以不会形成闭环(此结果为总结性,具体图还在研究中)

ConcurrentHashMap为什么能解决安全问题,在jdk1.8做了哪些优化?

下图为jdk1.7的concurrentHashMap的原理图

 

下面图为jdk1.8对concurrentHashMap优化后的结构图

 总结:

jdk7 ConcurrentHashMap 基于segment数组 里面是很多segMent数组 每一个segMent里面都有一个Hashtable数组 每一个Hashtable数组里面有多个HashEntry 每一个hashEntry后面都有一个链表 每一个segMent去继承 ReenTrantLock 保证并发安全(相当于分段锁) 这个也concurrentHashMap保证线程安全的原因

jdk8 优化 优化成node节点的类型的数组(ConcurrentHashMap基于node节点的数组) 去掉了hashTable概念 没有段的概念了 每一个node节点要么就是链表 要么就是红黑树 jdk8 当put新值得时候 在hash计算新值索引位置该放入哪个node得时候 或者扩展node链表得时候 会通过synchronized加在该节点头部(相当于就在整个桶(backet上面)) 然后遍历如果是链表的话判断是不是有变化如果有变化替换, 相当于锁得粒度更加细化了 如果没有变化新加一个节点到尾部 当在并发情况下 T1 T2同时插入头部节点 可能导致T2将T1得值覆盖 此时concurrentHashMap上面加了许多的CAS原子操作保证值不会丢失 里面还有一个sizectl值 当sizectl=-1的时候代表当前数组扩容还没有扩容完 当>0的时候数组可以扩容的

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值