jdk8起concurrenthashmap已移除segment分段锁,改用cas+synchronized实现桶级细粒度锁,扩容通过forwardingnode渐进迁移,sizectl兼具初始化、阈值与状态标识多重语义。

ConcurrentHashMap 的分段锁早已被移除,别再被 JDK 7 文档误导
JDK 8 起 ConcurrentHashMap 就不再使用 Segment,所谓“分段锁”是 JDK 7 的历史实现。当前主流(JDK 8–21)全部基于 CAS + synchronized 控制桶(bin)级粒度,sizeCtl 字段承担扩容协调职责。继续按“分段锁”理解扩容机制,会导致对迁移逻辑、线程协作和 ForwardingNode 作用的误判。
扩容触发条件:不是 size > threshold 就立刻扩容
扩容由 put 或 helpTransfer 触发,但真正开始需满足两个条件:
-
sizeCtl值为负数且小于 0 —— 表示已有线程在扩容中; - 当前
table长度未达最大容量(2^30),且新元素插入后总节点数(通过baseCount+CounterCell[]统计)超过阈值sizeCtl(即0.75 × capacity)。
注意:size() 返回的是估算值,不阻塞也不保证强一致;而扩容判断依赖的是更精确的计数器更新路径,不是简单调用 size()。
并发迁移靠 ForwardingNode,不是“复制完再切换”
扩容不是原子切换新旧数组,而是渐进式迁移:
- 每个线程尝试迁移时,先检查对应桶是否为
ForwardingNode—— 是则说明该桶已被其他线程处理,跳过; - 否则用
synchronized锁住该桶头节点,把其中所有Node拆成两组(低位/高位),分别插入新表的对应位置; - 迁移完成后,将原桶头设为
ForwardingNode,其nextTable字段指向新表,hash值固定为-1; - 后续
get遇到ForwardingNode会直接去新表查,put则协助迁移剩余桶。
这意味着:新旧表可能长期共存;读操作全程无锁且自动适配迁移状态;写操作只锁单个桶,不阻塞其他桶的读写。
红黑树在扩容中如何处理?TreeBin 不参与迁移逻辑
红黑树节点实际存储在 TreeBin 实例中,而 TreeBin 本身是普通 Node 子类,被当作一个整体迁移:
- 迁移时,整个
TreeBin被拆分为低位/高位两棵子树(依据 key 的 hash 与旧容量做 & 运算); - 若子树节点数 ≤ 6,则转回链表;否则保持红黑树结构放入新表对应桶;
-
TreeBin的读写锁(lockRoot/unlockRoot)仅用于树内增删平衡,不影响扩容时的桶级同步。
关键点:红黑树不是“升级后就脱离并发控制”,它仍受 synchronized 桶锁保护;迁移过程也不重建树结构,只是按位拆分——这点和 HashMap 的红黑树迁移逻辑一致,但多了一层 TreeBin 封装。
真正容易被忽略的是 sizeCtl 的多语义:它既是初始化容量、又是扩容阈值、又是扩容中标志位(-1 表示正在初始化,










