并发编程之深入理解ConcurrentHashMap

1 介绍

        ConcurrentHashMap是线程安全的HashMap

        存储的结构:数组 + 链表 + 红黑树

        ConcurrentHashMap在JDK1.8中是以CAS+synchronized实现的线程安全

                CAS:在没有hash冲突时(Node要放在数组上时)

                synchronized:在出现hash冲突时(Node存放的位置已经有数据了)

2 参数解读

2.1 hash值:散列算法

        将高位的hash也参与到计算索引位置的运算当中,降低hash碰撞概率

        hash值默认情况下一定是正数,在ConcurrentHashMap中,hash值为负数有特殊含义

                ① static final int MOVED  = -1:代表当前hash位置的数据正在扩容

                ② static final int TREEBIN  = -2:代表当前hash位置下挂载的是一个红黑树

                ③ static final int RESERVED  = -3:代表当前索引位置已经被占用,但是值还没有放进去

2.2 sizeCtl:标识数组初始化和扩容信息

        ① -1:代表当前数组正在初始化

        ② 0:代表数组还没初始化

        ③ 小于-1:代表正在扩容(低16位代表当前数组正在扩容的线程个数 - 如果1个线程扩容,值为-2,如果2个线程扩容,值为-3)

        ④ 大于0:代表当前数组的初始化大小(当前数组还没有初始化),或当前数组的扩容阈值(数组已经初始化)

3 源码解读

        3.1 put方法总结

        3.2 get方法总结

4 注意点

        4.1 JDK1.8 版本改进

                在 JDK1.7 版本中,ConcurrentHashMap 由数组 + Segment + 分段锁实现,Segment 通过继承 ReentrantLock 来进行加锁,通过每次锁住一个 segment 来降低锁的粒度而且保证了每个 segment 内地操作的线程安全性,从而实现全局线程安全。

                在JDK1.8中,取消了分段锁的设计,而是以CAS+synchronized实现的线程安全,在没有hash冲突时(Node要放在数组上时)使用CAS操作;在出现hash冲突时(Node存放的位置已经有数据了)使用synchronized。

        4.2 初始化数组

                ① new时,不会创建数组,在使用时才会创建(懒加载)

                ② 数组长度必须是2的n次幂,如果不是,底层会调用tableSizeFor(int c),将数组长度转换为2的n次幂 

        4.3 调用put方法添加key和value时,key 和 value 不允许为 null

                ConcurrentHashMap 作为并发容器,必须通过设计规避因 null 值导致的逻辑漏洞和性能损耗,在并发场景下保证其线程安全

        4.4 链表转红黑树

                当链表长度超过8个,会转换成红黑树。但是在转换之前,先判断数组长度是否小于64,如果小于不转成红黑树,先进行扩容。(还是更希望数据存放在数组上,这样查询效率更高)

                转红黑树,将单向链表Node对象转换为TreeNode对象,在通过TreeBin方法转为红黑树,TreeNode对象中还维护了一套双向链表。(这是为了方便将红黑树再转换为单向链表,红黑树为了保持平衡,会出现旋转,指针就会转换)

        4.5 扩容标识戳作用

                基于老数组长度计算扩容标识戳

                        ① 为了保证后面的sizeCtl赋值时,保证sizeCtl为小于-1的负数(小于-1:代表正在扩容)

                        ② 用来记录当前是从什么长度开始扩容的

                当某线程发起扩容时,会基于当前戳值生成唯一标识,其他线程在协助扩容前需验证此标识是否一致。若不一致,说明存在更高优先级的扩容操作(如容量更大的扩容),需放弃当前操作‌。

                扩容标识戳通过 ‌代数标记、容量绑定、原子操作‌ 等设计,解决了多线程环境下扩容任务的 ‌协调性、一致性和高效性‌ 问题,是ConcurrentHashMap实现无锁化并发扩容的核心机制‌。

        4.6 首次扩容为什么计数要 +2 而不是 +1

                低16位在表示当前正在扩容的线程有多少个,每一个线程扩容完毕后,会对低16位进行-1操作,当最后一个线程扩容完毕后,减1的结果还是-1。

                当值为-1时,最后一个线程要对老数组进行一波扫描,查看是否有遗漏的数据没有迁移到新数组。

完整原图分享

https://kdocs.cn/l/ccEGsn5LHN43

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值