ConcurrentHashMap扩容过程中如何保证更新一致性

在 ConcurrentHashMap(CHM)的扩容过程中,更新一致性(即:不会丢失写入、不会读到中间状态、所有线程看到一致的数据视图)是通过一系列精巧的并发控制机制实现的。下面从 写操作(put/update) 和 读操作(get) 两个维度,深入解析扩容期间如何保证更新一致性。

一、核心原则:迁移完成前,旧表仍有效

CHM 扩容采用 “双表并存 + 原子切换” 策略:

  • 扩容期间,旧表(table)和新表(nextTable)同时存在
  • 只有当一个桶(bucket)迁移完成后,才将旧表该位置替换为 ForwardingNode
  • 在此之前,所有对该桶的读写仍作用于旧表

✅ 这保证了:任何时刻,每个 key 要么在旧表,要么在新表,不会“消失”或“分裂”

二、写操作(put/update)的一致性保障

当线程执行 put(key, value) 时:

情况 1:目标桶尚未迁移(仍是普通 Node)

  • 直接在 旧表 中插入或更新
  • 后续迁移线程会将这个新写入的节点 一起迁移到新表
  • ✅ 不会丢失更新

情况 2:目标桶已迁移(头节点是 ForwardingNode)

  • 当前线程调用 helpTransfer() 协助扩容
  • 扩容完成后,重新执行 put 操作(循环 retry)
  • 此时 key 会直接插入到 新表 的正确位置
  • ✅ 最终写入成功,且只存在于一个地方

🔁 关键:putVal() 方法内部是一个 for(;😉 循环,直到成功插入为止。

for (int i = 0;; ++i) {
if (tab == null || tab.length == 0)
initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, newNode(hash, key, value, null)))
break;
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f); // 协助扩容后,tab 指向新表,继续循环
else {
// 锁住 f,插入或更新
}
}

💡 结论:无论扩容是否发生,put 操作最终都会成功写入唯一一份数据,不会重复、不会丢失。

三、读操作(get)的一致性保障

get(key) 在扩容期间完全无锁,但依然强一致:

  1. 如果桶未迁移
  • 直接从旧表读取 → 正确
  1. 如果桶已迁移(ForwardingNode)
  • 调用 ForwardingNode.find(hash, key)
  • 该方法会 自动跳转到 nextTable 查找
  • ✅ 读到的是最新值
  1. 如果桶正在迁移(但尚未 CAS 成功)
  • 由于迁移是 最后一步才设置 ForwardingNode
  • 此时桶仍是普通链表 → 从旧表读取 → 正确

📌 关键点:

  • ForwardingNode 的设置是 原子的(CAS)
  • Node.next 是 final 的,链表结构不可变
  • table 和 nextTable 字段都是 volatile,保证可见性

因此,get 永远不会看到“部分迁移”的链表,也不会读到 null 或错误值。

四、并发写 + 扩容:如何避免覆盖?

假设有两个线程:

  • 线程 A:正在迁移桶 X
  • 线程 B:同时对桶 X 执行 put

执行顺序分析:

  1. 若线程 B 先执行:

    • 桶 X 还是普通节点 → B 成功写入旧表
    • 线程 A 迁移时会包含 B 写入的新节点 → 一致
  2. 若线程 A 先完成迁移(设置 ForwardingNode):

    • 线程 B 发现 MOVED → 协助扩容 → 重试 put
    • 重试时直接写入新表 → 一致
  3. 若线程 A 正在迁移,但未完成:

    • 桶 X 仍是原链表 → B 写入旧表
    • A 的迁移是 遍历当前完整链表,会包含 B 的写入
    • 最后 CAS 设置 ForwardingNode → 原子切换

✅ 无论如何调度,最终结果都等价于某个顺序执行,满足 线性一致性(Linearizability)

五、特殊场景:红黑树迁移

  • 树节点通过 TreeBin 包装,TreeBin 内部使用 synchronized 保护树结构
  • 迁移时,TreeBin 会加锁遍历整棵树,确保快照一致性
  • 迁移后,新表中的树结构完整,旧表被替换为 ForwardingNode

🔒 树操作本身有同步,迁移过程不会破坏红黑树性质。

六、总结:更新一致性的四大支柱
机制 作用
ForwardingNode 原子标记 明确区分“已迁移”和“未迁移”状态

CAS + volatile 保证状态切换的原子性与可见性

写操作 retry 机制 确保最终写入成功且唯一

不可变链表结构(final next) 避免读到中间状态

🌟 最终效果:

  • 写不丢:所有 put 最终都会持久化
  • 读不错:get 总能返回最新已提交的值
  • 无脏读/幻读:不会看到半迁移的数据
  • 高并发:全程无全局锁,扩容与读写并行

这就是 ConcurrentHashMap 被誉为“并发容器之王”的核心原因之一。

如需进一步了解 computeIfAbsent、merge 等复合操作在扩容中的一致性保证,也可以继续提问!

posted @ 2026-03-06 12:25  七星6609  阅读(0)  评论(0)    收藏  举报