一、为什么引入红黑树
链表过长时(时间复杂度为O(n)),搜索性能会很差,红黑树的搜索的时间复杂度为Olog(n);
二、红黑树转换时机
链表过长会转换成红黑树,转换要求:
- 数组长度 >= 64, 数组长度小于64,会优先扩容数组
- 链表节点数 >= 8
三、扩容
- 创建一个新数组,长度是老数组的二倍。
- 将老数组中的数据,迁移到新数组中。
四、扩容时机
- 链表节点数 >= 8,数组长度 < 64
- 当map中的元素个数超过阈值时,就会除非扩容。阈值 = 数组长度*负载因子
- putAll方法时,如果写入的Map过大,会优先触发扩容,在将元素一个一个的写入
五、sizeCtl属性
表示三个信息
- sizeCtl == -1:表示数组正在初始化
- sizeCtl < -1:代表数组正在扩容
- sizeCtl > 0:有两种情况
-
- 数组未初始化,sizeCtl == 0代表数组没有指定具体的长度,采用默认的长度为16,sizeCtl > 0表示数组指定的初始化长度。
-
- 数组已初始化,下次扩容的阈值(= 数组长度 * 负载因子)
六、 ConcurrentHashMap数组初始化
map创建出来后,数组不会立即被创建,而是随着一次的put操作,才会将数组创建出来。懒加载的效果。
在执行put操作时,会判断数组是否为null,或者长度是否为0,如果为null或者为0,那就去创建数组。
七、DCL机制保证初始化线程安全
cas
八、计算数组下标
i = (n - 1) & hashcode
本质就是:(数组长度 - 1) & (key的hashcode)
散列算法的本质就是让高位也能参与到计算索引位置的过程里,运算方式很简单:hashcode ^ (hashcode >>> 16)
散列算法的目的是为了减少哈希冲突,在CHM中,所谓的hash冲突就是出现了两个key不同,但是确认索引位置相同的情况,散列算法的本质就是让key的hashcode值的高低位做异或运算,让高位也参与到计算索引位置的过程中,从而减少hash冲突。
九、ConcurrentHashMap写入操作的并发安全
1.7 是通过 segment 实现的。
1.8 是锁的Node对象,通过cas+synchronized实现的。
十、计数器线程安全的实现方式
CHM没有直接引用LongAdder,LongAdder功能比较多,CHM不需要那么多,将一些核心代码复制到了CHM中,借鉴 LongAdder 的设计,通过"分治法"来解决高并发下的计数竞争问题
CHM中由成员变量baseCount,和CounterCell的数组里面的value,组成了多个存储元素个数的位置,当CAS操作时,可以选择任意位置去做++或者--的操作。
只是最后统计的时候,需要将baseCount跟CounterCell进行累加。
- 核心组件:BaseCount 与 CounterCell
为了统计元素个数,ConcurrentHashMap 维护了两个核心变量:
baseCount:基础计数器。
在低并发(没有竞争)的情况下,线程会直接通过 CAS 操作更新这个变量。
CounterCell[]:分散计数单元数组。
在高并发(CAS 更新 baseCount 失败)的情况下,为了避免所有线程都去争抢 baseCount,系统会将计数压力分散到这个数组中。每个线程会绑定到数组中的某个元素(Cell)上进行更新。 - 统计流程:addCount(写入时)
当你执行 put 或 remove 操作时,会调用 addCount 方法来更新总数。流程如下:
尝试更新 baseCount:
首先尝试通过 CAS 操作直接更新 baseCount。
竞争失败 -> 降级到 CounterCell:
如果 CAS 失败(说明有其他线程也在更新),或者 CounterCell 数组已经初始化,线程会尝试更新 CounterCell 数组中的某个位置。
通常通过 ThreadLocalRandom.getProbe() & (数组长度-1) 来定位当前线程应该更新数组中的哪个格子。
这就像是在银行办事,如果"总窗口"(baseCount)排长队,就分流到旁边的"分窗口"(CounterCell)去办理。
扩容检查:
在更新计数的同时,还会检查当前总数是否超过了扩容阈值,如果超过则触发扩容。 -
获取流程:sumCount(读取时)
b
当你调用 size() 方法时,ConcurrentHashMap 会调用 sumCount() 方法来计算总数。
计算公式:
T
o
t
a
l
a
s
e
C
o
u
n
t
∑
(
C
o
u
n
t
e
r
C
e
l
l
.
v
a
l
u
e
)
Total=baseCount+∑(CounterCell[].value)
实现逻辑:
它会遍历 CounterCell 数组,将所有 Cell 的值累加到 baseCount 上。
- 关键特性:弱一致性
由于统计过程不加全局锁,size() 返回的结果是一个近似值(弱一致性)。
原因:在累加 baseCount 和 CounterCell 的过程中,可能又有其他线程完成了插入或删除操作。
结果:size() 返回的是调用时刻的一个估算值,可能略小于或略大于实际值,但在高并发场景下,这种微小的误差是可以接受的,换取的是极高的性能。
- 版本对比:JDK 1.7 vs JDK 1.8
表格
特性 JDK 1.7 (分段锁) JDK 1.8 (CAS + synchronized)
统计方式 遍历所有 Segment baseCount + CounterCell 数组
锁机制 需要尝试多次无锁统计,失败则锁住所有 Segment 全程无锁(CAS + 分散更新)
性能 数据量大时性能较差 极高,适合高并发
准确性 最终尝试加锁后可获得精确值 始终返回近似值(弱一致性)
总结
ConcurrentHashMap 的个数统计通过"化整为零"的策略,将原本集中的计数压力分散到多个 CounterCell 上,极大地降低了多线程环境下的竞争,从而实现了高效的并发统计。