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、如果定位的索引位置是一个标记(标记为正在扩容)
直接基于标记定位到新数组的位置,去新数组找数据。