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时,最后一个线程要对老数组进行一波扫描,查看是否有遗漏的数据没有迁移到新数组。
完整原图分享