1. 数据结构基础(JDK1.8)
- 底层结构:由
Node<K,V>[] table数组、链表、红黑树组成,数组大小始终为 2 的幂次(如 16、32)。 - 并发控制:摒弃 JDK1.7 的分段锁(Segment),改用
CAS + synchronized,锁粒度更小(锁定链表或红黑树的头节点)。
2. 扩容触发条件
-
阈值计算:初始容量为 16,负载因子默认为 0.75,扩容阈值为
容量×负载因子(如 16×0.75=12)。当元素数量超过阈值时触发扩容。 -
sizeCtl 参数:
sizeCtl=-1:表示数组正在初始化;sizeCtl> 0:表示初始化或扩容的阈值;sizeCtl=-N:表示有 N-1 个线程正在参与扩容(N 为扩容线程数)。
3. 扩容核心过程(重点)
假设原数组大小为oldCap,扩容后为newCap=oldCap×2,过程如下:
(1)触发扩容
当向 map 中添加元素时,通过addCount()方法检查元素数量是否超过阈值,若超过则调用transfer()方法启动扩容。
(2)分段迁移与线程协作
- 分割任务:将原数组分为多个迁移段(每个段大小为
oldCap/16),每个线程处理一段。 - ForwardingNode 标记:在原数组位置放置
ForwardingNode,表示该位置已被迁移,其他线程访问时会被引导到新数组。
(3)数据迁移逻辑
对每个节点e:
- 若
e为null:新数组对应位置直接置null; - 若
e为ForwardingNode:跳过(已被其他线程处理); - 若
e为链表节点:遍历链表,重新计算哈希值,根据新数组大小决定节点在新数组中的位置(因newCap=oldCap×2,哈希值的高位决定是否迁移到高位区间); - 若
e为红黑树节点:先转化为链表再迁移,若迁移后链表长度≥6,重新转为红黑树。
(4)扩容完成与状态更新
当所有段迁移完成后,将table指向新数组,更新sizeCtl为新的阈值,原数组被垃圾回收。
4. 并发安全与优化策略
-
并发控制:
- 迁移过程中,通过
CAS更新sizeCtl控制线程数,避免多线程冲突; - 对单个节点的操作使用
synchronized锁定,确保数据迁移时的原子性。
- 迁移过程中,通过
-
性能优化:
- 分段扩容:多个线程并行处理不同段,减少单线程负载;
- 懒迁移:允许线程在处理完自己的段后,协助其他线程迁移,避免扩容阻塞;
- 哈希高位利用:扩容时通过哈希值高位直接定位新位置,无需重新计算哈希,提升效率。
5. 与 HashMap 扩容的对比
- HashMap 扩容:单线程操作,扩容时会复制所有元素,期间无法读写;
- ConcurrentHashMap 扩容:多线程并行迁移,迁移过程中允许读写(读操作会被
ForwardingNode引导到新数组),无全局锁阻塞,并发性能更优。
6. 扩容流程示意图
- 扩容前数据结构:
- 扩容过程:高低位链表拆分:
- 扩容后的数据结构:
- 扩容核心流程时序图:
7. 核心原理总结
-
触发条件:元素数量超过
容量 × 负载因子(默认 16×0.75=12) -
扩容规则:容量翻倍(16→32→64...)
-
位置计算:
- 若
hash & oldCap == 0:新位置 = 原位置 - 若
hash & oldCap != 0:新位置 = 原位置 + oldCap
- 若
-
效率优化:
- 无需重新计算哈希值,仅通过位运算定位
- 一次遍历将链表拆分为高低位两部分,减少操作次数
这种设计使得 ConcurrentHashMap 在扩容时的性能显著优于传统哈希表,尤其在处理大规模数据时优势更加明显。