ConcurrentHashMap 1.8 彻底重构了扩容逻辑,抛弃了 1.7 的分段锁 + 全表单线程扩容 ,采用无锁 + CAS + 细粒度锁 实现多线程并行扩容,把扩容性能瓶颈从 O (n) 压到极致,是高并发场景下的核心优化。
我用极简原理 + 核心流程 + 关键细节的方式,清晰拆解这套高效扩容机制:
一、核心设计:为什么 1.8 扩容能做到高效?
- 数据结构升级:数组 + 链表 / 红黑树,不再是 Segment 分段锁
- 扩容核心思想 :不阻塞写操作 + 多线程分摊迁移任务
- 关键标识 :用
sizeCtl控制扩容状态、用transferIndex分配迁移桶 - 最小迁移单元 :一个桶(链表 / 红黑树),线程只锁当前桶,不锁全表
二、核心扩容变量(看懂这 4 个变量就懂了扩容)
| 变量 | 作用 |
|---|---|
| sizeCtl | 扩容状态控制:-1 = 正在初始化;<0 = 多线程扩容中(-N 代表 N 个线程);>0 = 下一次扩容阈值 |
| transferIndex | 待迁移桶的起始索引,线程通过 CAS 抢占迁移任务,保证不重复、不遗漏 |
| nextTable | 扩容后的新数组(容量是原数组 2 倍),扩容完成后替换原 table |
| fwd 节点(ForwardingNode) | 标记当前桶已迁移完成,其他线程访问会被引导去新数组操作 |
三、多线程并行扩容完整流程(最核心)
1. 触发扩容
- 元素个数达到阈值
sizeCtl - 单个桶链表长度≥8 且数组长度 < 64
- 写线程(put/remove)发现扩容中,自动协助扩容(核心:无专门扩容线程,业务线程兼职扩容)
2. 初始化扩容(第一个触发扩容的线程)
- CAS 修改
sizeCtl为-1,独占初始化权 - 创建
nextTable(容量 = 原容量 ×2) - 设置
transferIndex=原容量(从最后一个桶往前迁移) - 重置
sizeCtl为-(1 + 参与扩容的线程数),标记扩容开始
3. 多线程抢占迁移任务(并行核心)
- 所有业务线程(put/get/remove)发现扩容,都会加入协助扩容
- 线程通过CAS 修改 transferIndex,批量领取迁移桶(默认一次领取 16 个)
- 领取规则:从后往前,每个桶只被一个线程迁移,无竞争
- 所有线程分摊迁移任务,容量越大,并行效率越高
4. 单桶迁移逻辑(无锁 + 最小粒度锁)
- 线程获取当前桶的头节点,加 synchronized 锁(只锁头节点)
- 把当前桶的链表 / 红黑树拆分成 2 份 :
- 低桶:
hash & 原容量 == 0→ 留在新数组原位置 - 高桶:
hash & 原容量 != 0→ 迁移到「原位置 + 原容量」
- 低桶:
- 迁移完成后,将原桶置为fwd 节点,标记完成
- 释放锁,继续迁移下一个桶
5. 扩容期间的读写操作(完全不阻塞)
- 读操作 :遇到 fwd 节点,直接去
nextTable读取,无锁 - 写操作 :遇到 fwd 节点,协助扩容,再执行写入
- 只有正在迁移的桶会加锁,其他桶完全并行
6. 扩容完成
- 所有桶迁移完毕
- 将
table指向nextTable - 重置
sizeCtl为新阈值(新容量 ×0.75) - 销毁临时变量,扩容结束
四、1.8 多线程扩容的 3 个革命性优化
1. 业务线程兼职扩容,无单独扩容线程
不用等待后台线程扩容,所有线程一起加速,容量越大越快。
2. 最小粒度锁,无全表阻塞
只锁迁移中的桶头节点,99% 的操作无锁并行,并发能力爆炸。
3. 迁移过程无阻塞读写
读操作无缝跳转新数组,写操作协助扩容,彻底解决 1.7 单线程扩容的性能瓶颈。
五、对比 1.7:性能差距有多大?
| 特性 | JDK 1.7 ConcurrentHashMap | JDK 1.8 ConcurrentHashMap |
|---|---|---|
| 扩容方式 | 单线程全表迁移,全程阻塞 | 多线程并行迁移,无全表阻塞 |
| 锁粒度 | Segment 分段锁 | 桶头节点 synchronized + CAS |
| 扩容性能 | O (n),高并发严重卡顿 | O (n/M),M = 参与扩容线程数 |
| 读写阻塞 | 扩容期间阻塞所有操作 | 扩容期间读写几乎无感知 |