# 详解 CocurrentHashMap 的 ForwardingNode
详解 CocurrentHashMap 的 ForwardingNode
CocurrentHashMap 扩容时,如何保证其他线程对该扩容操作的感知呢?
ForwardingNode 节点是扩容过程中的关键组件,主要用于协调多线程迁移数据并确保线程安全。
核心作用
-
标识迁移状态:当某个桶(bucket)的数据迁移到新表后,原位置会被替换为
ForwardingNode,标记该桶已处理完毕。其他线程遇到此节点时,会感知到扩容正在进行,并转向新表操作。 -
协助扩容:线程在操作(如
put、remove)时若发现ForwardingNode,会先协助完成数据迁移,再继续自身操作,提升扩容效率。 -
查询转发:在
get操作中,遇到ForwardingNode会直接跳转到新表查询,确保数据访问的正确性。
结构与实现
-
类定义:
static final class ForwardingNode<K,V> extends Node<K,V> { final Node<K,V>[] nextTable; // 指向扩容后的新表 ForwardingNode(Node<K,V>[] tab) { super(MOVED, null, null, null); // 哈希值标记为 MOVED(-1) this.nextTable = tab; } }- 继承自
Node,但哈希值固定为MOVED(-1),标识特殊状态。 - 包含
nextTable字段,指向扩容后的新数组。
- 继承自
-
线程安全机制:
- 迁移桶时,通过
synchronized锁住头节点,确保原子性替换为ForwardingNode。 - 其他线程访问时,若发现锁被占用,会等待或协助迁移,避免数据不一致。
- 迁移桶时,通过
操作流程示例
-
数据迁移:
- 线程迁移完某个桶后,用
ForwardingNode替换原头节点,并释放锁。 - 后续线程访问该桶时,直接跳转到
nextTable。
- 线程迁移完某个桶后,用
-
查找操作:
- 遇到
ForwardingNode后,调用其find方法,在新表中递归查找:Node<K,V> find(int h, Object k) { // 跳转到新表,处理可能的嵌套扩容 outer: for (Node<K,V>[] tab = nextTable;;) { // 在新表中定位桶并遍历节点 // 若再次遇到 ForwardingNode,继续跳转 outer,直到找到目标节点 } }
- 遇到
设计优势
- 无锁化协作:通过状态标记(
MOVED)和辅助迁移,减少锁竞争,提升并发性能。 - 平滑过渡:读操作无需加锁,直接根据
ForwardingNode转发到新表,保证高吞吐量。 - 高效扩容:多线程协同迁移数据,分摊开销,避免单线程瓶颈。
总结
ForwardingNode 是 ConcurrentHashMap 实现高效并发扩容的核心机制。它通过标记迁移状态、转发操作到新表,以及协调多线程协作,确保了扩容期间的高性能和线程安全。理解其工作原理有助于深入掌握 ConcurrentHashMap 的并发设计思想。
面试题
1. 问题:ForwardingNode 的主要作用是什么?它是如何实现高效并发扩容的?
- 核心作用:
- 标识迁移状态:标记某个哈希桶的数据已被迁移到新表。
- 转发查询/操作:在
get或遍历时,将操作重定向到新表。 - 协调多线程协作:其他线程发现
ForwardingNode后,会主动协助迁移数据。
- 实现高效扩容的关键:
- 无锁化设计:通过
ForwardingNode的状态标记(MOVED),其他线程无需阻塞即可感知扩容。 - 分摊迁移开销:多个线程可并行迁移不同桶的数据,避免单线程瓶颈。
- 原子替换机制:使用
synchronized锁住桶的头节点,确保替换为ForwardingNode的原子性。
- 无锁化设计:通过
2. 问题:ForwardingNode 的哈希值为什么被设为 MOVED(-1)?这种设计的意义是什么?
- 哈希值标记:
ForwardingNode的哈希值固定为MOVED(-1),这是一个特殊常量,用于快速识别节点类型。 - 设计意义:
- 快速判断:在访问桶时,通过检查节点的哈希值是否为
MOVED,可立即知道当前处于扩容状态。 - 避免冲突:正常节点的哈希值不会为负数,因此
MOVED作为唯一标识,避免与用户数据冲突。 - 简化逻辑:无需通过
instanceof判断节点类型,直接通过哈希值即可确定操作路径。
- 快速判断:在访问桶时,通过检查节点的哈希值是否为
3. 问题:在数据迁移过程中,如何保证线程安全?ForwardingNode 如何参与这一过程?
- 线程安全机制:
- 锁粒度:迁移时通过
synchronized锁住桶的头节点,确保同一时间只有一个线程操作该桶。 - CAS 操作:在替换头节点为
ForwardingNode时,通过 CAS 保证原子性。 - 协助迁移:其他线程发现
ForwardingNode后,会调用helpTransfer()协助迁移其他未完成的桶。
- 锁粒度:迁移时通过
- ForwardingNode 的角色:
- 完成标记:桶迁移完成后,原位置替换为
ForwardingNode,表示该桶已处理。 - 转发查询:后续线程的读操作直接跳转到新表,写操作需等待迁移完成。
- 完成标记:桶迁移完成后,原位置替换为
4. 问题:在 get 操作中遇到 ForwardingNode 时,ConcurrentHashMap 如何处理?是否可能多次跳转?
- 处理流程:
- 跳转新表:调用
ForwardingNode.find(),直接在新表nextTable中继续查找。 - 递归处理:若新表中再次遇到
ForwardingNode(嵌套扩容),则继续跳转到更新的表。
- 跳转新表:调用
- 多次跳转的可能性:
可能发生。例如在并发扩容时,旧表扩容到新表,而新表自身也在扩容,形成“嵌套扩容”。find()方法通过循环逻辑处理这种情况,直到找到实际数据节点或null。
5. 问题:ForwardingNode 和其他节点(如 TreeNode)有什么区别?为什么它不存储实际数据?
- 区别对比:
特性 ForwardingNode TreeNode 用途 扩容占位符 红黑树节点 存储数据 不存储 存储键值对 哈希值 固定为 MOVED(-1)正常哈希值(≥0) 结构关联 指向新表 nextTable维护红黑树父子/兄弟指针 - 不存储数据的原因:
- 职责单一:仅作为扩容状态标识和操作转发,无需冗余存储数据。
- 内存优化:避免无效数据占用空间,提升内存利用率。
- 逻辑清晰:通过
nextTable直接关联新表,保证跳转路径简洁。

浙公网安备 33010602011771号