sizectl为负数时分三种状态:-1表示正在初始化;-(1+n)表示n个线程正协助扩容;高16位为resizestamp、低16位为参与线程数,且需结合nexttable与transferindex判断是否真在迁移。

sizeCtl 为负数时,到底在表示什么状态
看到 sizeCtl 是负数,不能一概认为“正在扩容”。它实际承载三种互斥状态,靠具体值区分:
-
sizeCtl == -1:唯一明确的初始化中状态,此时table为空,首个线程正通过CAS尝试构建数组 sizeCtl 且高 16 位非零:表示扩容中,高位是扩容戳(<code>resizeStamp(n)),低位是参与线程数(如-5表示已有 4 个线程在协助)-
sizeCtl == 0:未初始化且未指定初始容量,下次put会按默认 16 构建
常见误判是把 sizeCtl == -1 当作扩容失败或卡死,其实这只是初始化锁的标记;真正扩容开始后,值会立刻变成类似 -2147483646 这种带戳的大负数。
为什么扩容时 sizeCtl 要用位运算拼接线程数和扩容戳
因为要在一个 int 里塞两个信息,又必须保证 CAS 更新原子性。JDK 选择高位存戳、低位存线程计数,靠 RESIZE_STAMP_SHIFT = 16 隔开:
- 扩容戳由
resizeStamp(n)生成,确保每次扩容(哪怕同一容量)戳都不同,避免旧扩容任务被误续 - 线程数只占低 16 位,最大支持
MAX_RESIZERS = (1 个协作者,实际远达不到 - 每次有新线程加入,执行
CAS(sizeCtl, old, old + 1)—— 注意不是设固定值,而是自增,所以必须位分离,否则会覆盖戳
如果直接用 AtomicInteger 存线程数,就无法在单次 CAS 中同时校验“是否同一次扩容”和“当前参与数”,这是设计硬约束。
如何从 sizeCtl 值反推当前 ConcurrentHashMap 的真实状态
别依赖日志或调试器看 sizeCtl 的原始数字,要拆解判断:
- 若
sizeCtl > 0:看是否已初始化table。已初始化 → 这是下一次扩容阈值(capacity * 0.75);未初始化 → 这是用户传入的初始容量(会被提升到最近 2 的幂) - 若
sizeCtl == -1:检查table == null || table.length == 0,为真则确实在初始化;否则说明初始化异常中断(极罕见) - 若
sizeCtl :提取高 16 位(<code>sizeCtl >>> 16),与当前数组长度n对应的resizeStamp(n)比对,一致才确认是本次扩容;再取低 16 位(sizeCtl & 0xFFFF)得参与线程数
最容易忽略的是:扩容完成前,nextTable != null 且 transferIndex > 0 才算真正在迁移;仅凭 sizeCtl 只能说明扩容已启动,不保证数据在动。
sizeCtl 在 transfer 过程中怎么防止扩容任务被重复触发
关键不在 sizeCtl 本身,而在它和 nextTable、transferIndex 的协同:
- 首次扩容线程调用
transfer(tab, null)时,会先创建nextTable并设置transferIndex = nextTable.length,然后才更新sizeCtl - 后续线程进入
helpTransfer()或tryPresize(),会先检查nextTable != null且transferIndex > 0,满足才参与迁移;否则直接返回 -
sizeCtl的负值只是“准入信号”,真正干活要看transferIndex是否还有剩余段可分
所以即使多个线程几乎同时检测到元素超阈值,也只有一个能成功创建 nextTable 并推进 sizeCtl,其余都会降级为协助者——这个临界点藏在 nextTable 的非空判断里,不是单靠 sizeCtl 能 cover 的。










