JDK 1.8 ConcurrentHashMap通过sizeCtl标记、transferIndex分片、ForwardingNode占位实现多线程协作扩容:线程发现扩容中即调helpTransfer协助迁移,按桶区间分工,读写操作自动跳转新表,扩容完成时原子切换table引用。

ConcurrentHashMap 在 JDK 1.8 中的扩容机制不是“等一个线程干完”,而是设计成可被多个线程主动加入、分片协作、状态驱动的过程。它不依赖外部调度,而是靠几个核心字段 + 原子操作 + 协作协议,让线程在发现扩容正在进行时,能立刻“认领任务”并安全参与。
扩容如何被多线程识别和加入
当某个线程在 addCount 或 putVal 后检查到元素总数超过 sizeCtl(即触发阈值),会调用 tryPresize 尝试扩容。这时关键动作是:
- 若
sizeCtl是正数(表示正常状态),就用 CAS 尝试将其设为一个负的特殊标记值,即resizeStamp(n) ,成功者成为「扩容发起者」; - 若
sizeCtl已是负数(说明扩容已在进行),当前线程不会阻塞或放弃,而是直接调用helpTransfer方法,尝试协助迁移。
这个设计意味着:扩容不是“谁先抢到谁干”,而是“谁看到就帮一把”。
协助迁移怎么分工不冲突
所有参与迁移的线程共享同一个 nextTable(新数组)和一个关键控制变量 transferIndex(迁移起始下标)。迁移过程以「桶区间」为单位划分任务:
- 每次从
transferIndex倒序取一段连续桶(默认步长MIN_TRANSFER_STRIDE = 16),比如[i-16, i); - 用 CAS 更新
transferIndex,确保不同线程拿到互不重叠的区间; - 线程只负责自己分到的桶,逐个把旧表中该范围内的非空桶迁移到新表对应位置;
- 迁移完成后,会在原桶位置插入一个
ForwardingNode(hash = -1),作为“已迁移”占位符。
这样既避免了重复迁移,也无需全局锁协调。
迁移过程中读写操作怎么不乱
-
读操作(get):遇到
ForwardingNode,会直接跳转到nextTable继续查找,保证读到最新数据; -
写操作(put):若定位到的桶是
ForwardingNode,也会先帮忙迁移(调用helpTransfer),再继续插入,实现“写即协助”; -
新插入节点:在迁移未完成期间,仍允许往旧表写入,但只会写入尚未被标记为
ForwardingNode的桶;已迁移的桶由ForwardingNode拦截并导向新表。
也就是说,扩容不是“停写等待”,而是“边写边搬”,整个过程对业务线程透明。
扩容完成怎么判断和切换
-
sizeCtl的低 16 位记录当前正在协助的线程数(+1 表示有线程在干活); - 当
transferIndex 且所有桶都处理完毕(每个桶要么是ForwardingNode,要么为空),最后完成迁移的线程会:- 把
nextTable赋给table字段; - 重置
sizeCtl为新容量的阈值(0.75 * newCapacity); - 清空
nextTable引用。
- 把
这个切换本身是原子的赋值操作,后续所有读写自然落到新表上。
不复杂但容易忽略
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











