ConcurrentHashMap详解
JDK1.7之前的ConcurrentHashMap使用分段锁机制实现,JDK1.8则使用数组+链表+红黑树数据结构和CAS原子操作实现ConcurrentHashMap
concurrentHashMap的数据结构
一、jdk1.7
1、Jdk1.7 之前 java使用了分段锁机制实现
使用segment数组,将hash表划分为多个分段;segment 通过继承ReetrantLock来进行加锁,所以每次需要加锁的操作锁住的是一个segment,这样只要保证每个segment是线程安全的,就实现了全局的线程安全
2、concurrencyLevel: 并行级别、并发数、Segment 数,怎么翻译不重要,理解它。默认是 16,也就是说 ConcurrentHashMap 有 16 个 Segments,所以理论上,这个时候,最多可以同时支持 16 个线程并发写,只要它们的操作分别分布在不同的 Segment 上。这个值可以在初始化的时候设置为其他值,但是一旦初始化以后,它是不可以扩容的。
3、initialCapacity: 初始容量,这个值指的是整个 ConcurrentHashMap 的初始容量,实际操作的时候需要平均分给每个 Segment。 loadFactor: 负载因子,之前我们说了,Segment 数组不可以扩容,所以这个负载因子是给每个 Segment 内部使用的。
负载因子为什么为0.75?
4、扩容
segment 数组不能扩容,扩容是 segment 数组某个位置内部的数组 HashEntry<K,V>[] 进行扩容,扩容后,容量为原来的 2 倍。
二、Jdk1.8的concurrentHashMap
1、在JDK1.7之前,ConcurrentHashMap是通过分段锁机制来实现的,所以其最大并发度受Segment的个数限制。因此,在JDK1.8中,ConcurrentHashMap的实现原理摒弃了这种设计,而是选择了与HashMap类似的数组+链表+红黑树的方式实现,而加锁则采用CAS和synchronized实现。
2、通过提供初始容量,计算了 sizeCtl,sizeCtl = 【 (1.5 * initialCapacity + 1),然后向上取最近的 2 的 n 次方】。如 initialCapacity 为 10,那么得到 sizeCtl 为 16,如果 initialCapacity 为 11,得到 sizeCtl 为 32。
3、初始化数组initTable
主要就是初始化一个合适大小的数组,然后会设置 sizeCtl。
初始化方法中的并发问题是通过对 sizeCtl 进行一个 CAS 操作来控制的。
4、链表转红黑树“treeifyBin
如果数组长度小于 64 的时候,其实也就是 32 或者 16 或者更小的时候,会进行数组扩容
5、扩容 tryPresize
扩容后数组容量为原来的 2 倍 主要使用transfer方法
6、数据迁移transfer
将原来的 tab 数组的元素迁移到新的 nextTab 数组中。
7、get过程分析
1⃣️计算 hash 值
2⃣️根据 hash 值找到数组对应位置: (n - 1) & h
3⃣️根据该位置处结点性质进行相应查找
如果该位置为 null,那么直接返回 null 就可以了
如果该位置处的节点刚好就是我们需要的,返回该节点的值即可
如果该位置节点的 hash 值小于 0,说明正在扩容,或者是红黑树
如果以上 3 条都不满足,那就是链表,进行遍历比对即可