ConcurrentHashMap

本文详细探讨了ConcurrentHashMap的数据结构(数组+链表+红黑树)、如何保证写操作的线程安全、使用LongAdder的计数器实现以及其扩容机制,包括触发时机、步骤和读取数据的高效处理策略。
摘要由CSDN通过智能技术生成

ConcurrentHashMap

  1. 存储结构

  2.写操作线程安全

  3.计数器

  4.扩容

  5.获取数据

1、存储结构
HashMap和ConcurrenHashMap在存储结构上是一样的:数组 + 链表 + 红黑树
红黑树出现的原因

因为红黑树需要进行左旋、右旋、变色这些操作来保持平衡,而单链表不需要。

当元素小于 8 个的时候做查询操作时,链表结构能保证查询性能。

当元素大于 8 个的时候, 红黑树搜索时间复杂度是O(logn),而链表是 O(n),此时需要红黑树来加快查询速度,但是新增节点的效率变慢了。

所以啊,如果一开始就用红黑树结构,元素太少,这时新增效率也比较慢,会浪费性能。

为什么链表长度为8才转红黑树:
这个与hashcode碰撞次数的泊松分布有关系,是为了寻找一种时间和空间的平衡。
在负载因子是0.75(HashMap默认)的情况下,单个hash槽内元素个数为8的概率小于百万分之一,将7作为一个中间数,等于7时不做转换,大于等于8才转红黑树,小于等于6转链表。
链表中元素个数为8时的概率已经非常小,再多的就更少了,所以原作者在选择链表元素个数时选择了8,是根据概率统计而选择的。
红黑树结构情况下,如果删除元素,导致红黑树元素个数小于等于6,又会退化为链表。

默认加载因子为什么是0.75:

从时间和空间的角度综合得出的:

1.如果是1.0 当数组的值全部填充了才会发生扩容,此时Hash冲突是避免不了的。链表的操作或者红黑树的操作会牺牲时间来保证空间的利用率

2.如果是0.5 当数组中一半的数据利用了之后就会开始扩容。这时填充的数据少。hash冲突也会减少,底层的链表和红黑树的高度也会降低。查询效率增加。但是这时还有太多的空间没有利用。空间资源浪费了。

所以0.75是综合1和2的逻辑考虑得出。

2、写操作线程安全的保证方式
2.1 往数组上存数据,基于CAS保证安全
2.2 往链表/红黑树存数据,synchronized锁数组元素保证线程安全
2.3 JDK1.7中的ConcurrentHashMap是基于分段锁来保证的线程安全
3、计数器的实现
用LongAdder来保证线程安全
LongAdder底层就是基于CAS的方式,再进行+1、-1操作,当然能保证线程安全
AtmoicLong也能保证线程安全,为什么不用AtmoicLong呢?
比如ConcurrentHashMap中记录元素个数的是baseCount,如果有大量线程都想修改baseCount,基于CAS的方式,每次并发只会有一个线程成功,其他失败的线程需要再次获取baseCount的值,再执行CAS..如此反复。
AtmoicLong用的这种方式,其实这样是空转,会导致性能变慢。
这样的CAS操作,会浪费CPU的资源,降低性能。
LongAdder解决上述问题的方式就是,不让每个线程都对baseCount做CAS操作,LongAdder中
提供了很多的CounterCell对象,每个CounterCell内部都有一个long类型的value,线程在做计数
时,可以随机选择一个CounterCell对象对内部的value做+1操作,CounterCell数组的长度最长和你的CPU内核数一致。
CAS是CPU密集操作,能与CPU内核数 ± 1 匹配
baseCount + 所有CounterCell对象的value,最终结果等于ConcurrentHashMap中的元素个数。
4、扩容大致流程: 允许多个线程来并发扩容
4.1、扩容触发时机
链表到8。数组长度小于64,扩容数组。
0.75的负载因子,元素个数到了,就得扩。
执行putAll时,如果putAll中的map元素个数当前map无法放下,那就优先扩容。(跟0.75有关
系)将map.size做好运算,与当前的扩容阈值做比较,如果小于扩容阈值,直接添加,大于扩
容阈值,那就优先扩容。
4.2、计算扩容标识戳
标识戳后面会作为标记,代表当前ConcurrentHashMap内部正在扩容数组。
标识戳会记录当前是从多少长度的数组开始做扩容的,避免协助扩容时,出现错误。
4.3、计算每次迁移数据的步长,基于数组长度和CPU内核数计算,最小是16
每个线程会先领取一定长度的迁移数据的任务,领取完,一个位置一个位置的迁移。每次领取任
务的长度是多少,就基于步长来做的。
4.4、创建新数组,长度是老数组的二倍。
4.5、领取迁移数据的索引位置的任务,基于步长得出从哪个索引迁移到哪个索引。
4.6、开始将老数组数据迁移到新数组,等老数组的某个索引位置迁移完之后,会留下一个标记,标
记代表当前位置数据全部迁移到了新数组。
4.7、等老数组的所有数据,都迁移到新数组上之后,最后一个完成迁移数据的线程,会整体再检查
一遍老数组中有没有遗留的数据在。(基本没有) 4.8、最后检查完毕之后,迁移结束。
5、获取数据
ConcurrentHashMap在维护红黑树的同时,还会保留一个双向链表的数据结构,读操作,是不阻塞的:
1、若数据是在数组上,查询到就直接返回
2、若数据是在链表上,找到数组的索引位置后,next....next一个一个往下找,找到就返回
3、若数据在红黑树上
   3.1 如果有写线程在红黑树上进写数据操作,那么读线程去读取一个双向链表查询数据
   3.2 如果没有写线程在操作红黑树,那就在红黑树上正常的left和right左旋和右旋的去找对应数据
4、如果定位的索引位置是一个标记(标记为正在扩容)
     直接基于标记定位到新数组的位置,去新数组找数据。
  • 22
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值