ConcurrentHashMap 读操作遇到 ForwardingNode 时会自动跳转至新表继续查找,由 get() 内部的 Node.find() 完成重哈希与遍历,支持多轮递归跳转;ForwardingNode 无业务数据,val 和 key 均为 null,不可直接读取;其 nextTable 必不为 null,且并发读无竞争、零同步开销。

ConcurrentHashMap 读操作遇到 ForwardingNode 时怎么走
遇到 ForwardingNode 不代表出错,而是说明当前桶已完成迁移,读操作必须跳转到新表继续查找。这个过程完全由 get() 方法内部自动完成,无需手动干预。
关键逻辑在 Node.find(int h, Object k) 方法里:它拿到 nextTable 后,重新计算哈希在新表中的索引((n - 1) & h,其中 n 是新表长度),再遍历对应桶的链表或红黑树。如果新表该位置仍是 ForwardingNode(比如正在多轮扩容),就继续递归跳转,直到找到真实数据节点或返回 null。
为什么不能直接读 forwardingNode.val 或调用 get() 外部逻辑处理
ForwardingNode 继承自 Node,但它的 val 字段是 null,key 也是 null —— 它本身不存业务数据,只起状态标识和跳转作用。硬取 forwardingNode.val 会得到 null,且绕过跳转逻辑会导致查不到数据。
- 所有 public API(如
get()、containsKey())都已内置对ForwardingNode的识别和转发 - 手动 cast 成
ForwardingNode并调用find()属于内部协议,行为不保证稳定,JDK 版本升级可能调整其实现 - 调试时看到
ForwardingNode出现在 dump 中,只说明那个桶位已完成迁移,不是 bug
forwardingNode.nextTable 为空或被置 null 会怎样
nextTable 在扩容启动时就被初始化并赋值给 ConcurrentHashMap.nextTable 字段;只有当整个扩容彻底完成、新表正式接替旧表后,nextTable 才会被设为 null。所以只要看到 ForwardingNode,它的 nextTable 一定非空。
唯一例外是扩容中途 OOM 导致初始化失败,此时 sizeCtl 被设为 Integer.MAX_VALUE,后续 put 会拒绝写入,但已有 ForwardingNode 不会出现 nextTable == null —— 因为构造函数强制要求传入非空数组。
根据 package.json 中的 engines 字段自动检测 Node.js 版本需求,未安装 nvm 时自动安装并切换至对应版本。支持 Windows、macOS、Linux。适用于 Node.js 项目开发、版本切换和环境配置。
并发读时多个线程同时 hit 同一个 ForwardingNode 会不会卡住
不会。ForwardingNode.find() 是纯计算逻辑,无锁、无 CAS、不修改任何状态,只是读 nextTable + 重哈希 + 遍历。多个线程并发调用互不影响,甚至能共享 CPU cache line。
真正有竞争的是写操作(put、remove)遇到 ForwardingNode 时触发的协助扩容(helpTransfer),那才会去争抢迁移任务段。读操作全程零同步开销。
真正容易被忽略的是:ForwardingNode 的存在意味着你看到的「当前 table」只是扩容过程中的中间态,它的结构不可靠、不可遍历、也不该被反射或序列化工具直接读取 —— 所有业务代码必须通过标准 Map 接口访问,否则会踩进状态不一致的坑。










