在高并发场景下,ConcurrentHashMap(CHM)的扩容不仅要快,更要保证数据一致性—— 不能丢数据、不能读到中间状态、不能出现幻读或重复读。JDK 1.8 通过精心设计的并发迁移流程 + 内存屏障 + 原子操作,实现了强一致性写 + 弱一致性读的平衡。本文将深入剖析其一致性保障机制。
一、核心挑战:扩容期间如何避免数据不一致?
假设线程 A 正在 put,线程 B 正在 get,同时线程 C 触发了扩容。此时可能出现以下问题:
- 读线程读到“半迁移”状态(部分桶已迁,部分未迁);
- 写线程插入到旧表,但读线程去新表查,导致“丢失”;
- 多个线程同时迁移同一桶,造成数据覆盖或重复。
ConcurrentHashMap通过以下四大机制解决这些问题。
二、一致性保障机制详解
✅ 1. ForwardingNode:引导读写操作到正确位置
这是一致性最关键的基石。
- 当某个桶(bucket)的数据被迁移到新 table 后,原桶位置立即被替换为
ForwardingNode(hash = -1); ForwardingNode持有对新 table 的引用;- 任何线程访问该桶时:
- 若发现是
ForwardingNode,立即跳转到新 table 对应位置继续操作; - 写操作(put/remove)会协助完成剩余迁移;
- 读操作(get)直接在新 table 中查找。
- 若发现是
// get 操作中处理 ForwardingNode Node<K,V> f = tabAt(tab, i); if (f != null) { if (f.hash == MOVED) // MOVED = -1 return getNode(f.nextTable, key); // 跳转到新表 // ... 正常查找 }🔒效果:
- 无论桶是否迁移完成,所有操作都能定位到最新数据所在位置;
- 避免“旧表查不到、新表还没写”的数据丢失问题。
✅ 2. 桶级别迁移 + synchronized 锁头节点
- 迁移一个桶时,先对原桶的头节点加
synchronized锁; - 确保同一时间只有一个线程能迁移该桶;
- 迁移完成后,才将原桶设为
ForwardingNode。
synchronized (f) { // f 是原桶头节点 if (tabAt(tab, i) == f) { // 双重检查 // 迁移链表/红黑树到 nextTab setTabAt(nextTab, i, newHead); // 标记原桶已迁移 setTabAt(tab, i, fwd); } }🛡️作用:
- 防止多个线程重复迁移同一桶;
- 保证迁移过程的原子性;
- 避免 put 操作在迁移中途插入旧表导致数据丢失。
✅ 3. volatile + Unsafe 保证内存可见性
CHM 大量使用volatile和Unsafe的volatile 语义写(putObjectVolatile),确保多线程间的内存可见性:
table字段是volatile;ForwardingNode的设置通过setTabAt()(内部调用Unsafe.putObjectVolatile);nextTable在ForwardingNode中也是final(构造即可见)。
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) { U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v); }💡意义:
一旦一个线程将桶设为ForwardingNode,其他线程立即可见,不会因缓存读到旧值而误操作旧表。
✅ 4. 扩容期间的 put 操作:自动重定向 + 协助迁移
当 put 操作发现桶是ForwardingNode,它会:
- 调用
helpTransfer()协助完成当前扩容; - 然后重新执行 put 流程(此时 table 已更新为新表);
- 最终数据一定写入新 table 的正确位置。
else if (f.hash == MOVED) tab = helpTransfer(tab, f); // 协助迁移并返回新 table // 循环重新尝试 put✅结果:
即使扩容正在进行,所有新写入的数据都会进入新表,不会残留在旧表中。
三、读操作的一致性:弱一致但不错误
CHM 的get 操作不加锁,因此提供的是弱一致性(weakly consistent):
- 可能读到扩容前的旧值(如果迁移尚未完成);
- 但绝不会读到“损坏”或“中间状态”的数据;
- 也不会抛出
ConcurrentModificationException。
这是因为:
- 所有 Node 的
key、hash、val(除 compute 系列外)都是final 或 volatile; - 链表/红黑树结构在迁移时是整体替换,不会出现“断链”;
ForwardingNode确保读操作总能找到数据(无论新旧表)。
📌注意:
弱一致性 ≠ 不一致!它只是不保证“实时最新”,但保证读到的一定是某个合法历史状态。
四、扩容完成后的切换:原子更新 table 引用
当所有桶迁移完毕,最后一个完成任务的线程会:
- 将
nextTable赋值给table; - 清空
nextTable; - 重置
sizeCtl为新的阈值。
由于table是volatile,这一切换对所有线程立即可见,后续操作自然使用新表。
五、总结:CHM 如何做到“又快又稳”?
| 机制 | 作用 | 一致性保障 |
|---|---|---|
| ForwardingNode | 引导操作到新表 | 防止读写错位 |
| synchronized 锁桶头 | 串行化迁移 | 防止并发迁移冲突 |
| volatile / Unsafe | 内存可见性 | 确保状态变更及时同步 |
| put 自动重试 | 写入新表 | 避免数据残留旧表 |
| 弱一致读 | 高性能无锁 | 保证读到合法状态 |
💬一句话总结:
“通过 ForwardingNode 实现无缝跳转,通过细粒度锁保证迁移原子性,通过内存屏障确保可见性——三者结合,让扩容既高效又安全。”
视频看了几百小时还迷糊?关注我,几分钟让你秒懂!(发点评论可以给博主加热度哦)