concurrenthashmap扩容时不锁整个table,因其设计目标是高并发;采用分段迁移策略,每次仅锁单个bin,多线程协作迁移,通过transferindex分配区间,fwd节点标记已迁移桶,读操作遇fwd则重试新表,size()等非实时。

ConcurrentHashMap 扩容时为什么不是整体锁住整个 table?
因为 ConcurrentHashMap 的设计目标就是高并发读写,如果扩容时锁整个 table,会严重阻塞其他线程的 get、put 操作,违背其“分段锁”演进为“CAS + volatile + 细粒度锁”的初衷。
它采用的是「分段迁移」策略:每次只对一个 Node 数组槽位(即一个 bin)加锁,迁移其中的链表或红黑树节点到新数组对应位置。其他未被处理的桶仍可正常读写。
- 迁移由多个线程协作完成,通过
transferIndex原子递减来分配待处理的区间 - 每个线程负责一段连续的
tab下标范围,避免重复迁移 -
sizeCtl字段在扩容期间存储负值(如-1 - (resizeStamp ),既是状态标识也是参与线程计数器
扩容触发条件和 sizeCtl 的含义到底是什么?
扩容不是仅看当前 size 超过 threshold 就立刻开始,而是由 addCount 方法中 CAS 更新 baseCount 失败后,调用 tryPresize 触发。关键判断逻辑在 helpTransfer 和 transfer 入口:
- 当
tab != null && tab.length ,说明已有线程在扩容,当前线程可协助 -
sizeCtl = -1表示有线程正初始化;sizeCtl = -(1 + n)(n 是协助扩容的线程数)表示扩容进行中 - 初始扩容阈值计算为:
newCap = oldCap ,但实际容量增长受 <code>MAX_CAPACITY限制
注意:sizeCtl 不是简单阈值,它是复合状态字段——低 16 位存并发扩容线程数,高 16 位存扩容标记戳(由 resizeStamp 生成)。
立即学习“Java免费学习笔记(深入)”;
迁移过程中 get 操作还能正确返回值吗?
能。这是 ConcurrentHashMap 实现无锁读的关键所在。迁移时,旧数组对应桶会被设置为 FWD 类型的占位节点(ForwardingNode),其 nextTable 指向新数组。
当 get 遇到 FWD 节点时,会直接转到新数组对应位置查找,而不是阻塞等待迁移完成。
-
get完全不加锁,也不依赖volatile读整个数组,只靠单个Node的val和next字段的volatile语义保证可见性 - 迁移中写操作(如
put)若发现目标桶是FWD,也会主动帮助迁移或重试到新表 - 但要注意:迁移未完成时,
size()可能不准,因为它依赖对所有 bin 的遍历统计,而迁移中某些 bin 状态不稳定
Java 8 和 Java 9+ 的扩容机制有实质区别吗?
核心逻辑一致,都是多线程协作迁移 + FWD 节点引导读写,但 Java 9 开始引入了更激进的优化:
- Java 9+ 中
transfer使用了更细的步长控制(stride默认为NCPU > 1 ? (n >>> 3) / NCPU : n),避免单核机器上过度拆分任务 - 新增了
ReservationNode占位机制,在极端竞争下减少 CAS 失败重试次数 -
sizeCtl的编码方式未变,但resizeStamp计算增加了校验位(Integer.numberOfLeadingZeros(s) == 31),防止整数溢出导致误判
不过这些改动不影响你面试时回答主干逻辑——重点还是说清「谁触发」「怎么分摊」「如何不阻塞读」「FWD 怎么用」这四点。真正容易被忽略的是:迁移不是原子过程,size() 和 mappingCount() 返回的都不是严格实时值,生产中别拿它做强一致性判断。










