ConcurrentHashMap底层核心是putVal执行路径与volatile字段协同:先hash计算与table初始化(sizeCtl=-1),再分空桶(CAS)、扩容中(ForwardingNode→helpTransfer)、有节点(synchronized头结点)三路处理,辅以treeifyBin树化、transfer分段迁移及baseCount+CounterCell弱一致性计数。

想真正搞懂 ConcurrentHashMap 底层,不能靠零散看几段源码或背几个关键词(比如“CAS+synchronized”“分段锁”),而要带着问题、按执行路径、结合关键字段和状态流转去读。重点不是“它有哪些类”,而是“它怎么协作着把一个 put 操作安全、高效地做完”。下面这几个方向抓准了,读源码就不再晕头转向。
从入口方法开始,盯住 putVal 的完整流程
别一上来就翻 Node 或 TreeBin 类。直接打开 put(K key, V value) 方法,它最终会调用 putVal —— 这是整个并发写入逻辑的主干。你需要顺着它走完这四步:
- 先算 hash(注意 spread() 方法对高位参与运算的处理,这是为减少哈希碰撞)
- 检查 table 是否初始化,没初始化就调 initTable();这里会看到 sizeCtl 如何用 -1 标记正在初始化
- 定位桶位置后,分三种情况处理:桶为空(CAS 插入)、桶为 ForwardingNode(说明正在扩容,当前线程去帮 transfer)、桶有节点(synchronized 锁头节点再遍历)
- 插入后判断是否需要树化(链表长度 ≥8 且 table.length ≥64),并触发 treeifyBin()
重点关注几个核心 volatile 字段的语义和流转
ConcurrentHashMap 的线程安全性,本质上是靠几个被 volatile 修饰的关键字段来实现可见性和有序性。读源码时务必把它们的含义和变化时机理清楚:
- table:当前哈希表数组,volatile 保证所有线程看到的是最新引用
- nextTable:扩容时的新表,仅在 transfer 阶段非 null;它的存在让多线程能协作迁移数据
- sizeCtl:最易被忽略也最关键的状态控制器。它既是初始化标志(-1)、扩容阈值(正数)、扩容中标识(-N),也是协助扩容线程数的计数器(负数的低16位)
- baseCount + CounterCell[]:size() 返回估计值的原因就在这儿——并发更新计数不锁整张表,而是分散到多个 cell 上,最后 sum 一下;但这个 sum 不是原子快照,所以是估计值
把扩容(transfer)当作独立模块精读
线上 CPU 飙高、死循环等问题,八成出在扩容逻辑里。transfer 方法近 300 行,但它结构清晰:
- 每个线程负责迁移 table 中一段连续的 bin(stride 默认 16),避免争抢同一桶
- 迁移前先用 synchronized 锁住当前 bin 头节点,确保该桶不会被其他线程修改
- 迁移完成后,将原桶头设为 ForwardingNode(hash = MOVED),后续线程遇到它就会去帮着迁移别的桶
- 所有 bin 迁完,才把 nextTable 赋给 table,完成切换
对照关键状态节点,理解“运行时结构”如何动态变化
ConcurrentHashMap 不是静态结构,而是一套带状态机的数据组织方式。读源码时随时对照这些节点类型的实际作用:
- Node:普通链表节点,hash ≥ 0;是大多数数据的载体
- ForwardingNode:hash = -1,代表该 bin 已迁完或正在迁;它是扩容协作的“路标”
- TreeBin:红黑树的控制节点(不是 TreeNode),持有 root 和 lock,synchronized 锁的就是它;它内部维护真正的 TreeNode 链
- ReservationNode:hash = -3,用于特殊保留场景(如 computeIfAbsent 中占位),极少见但得知道它存在
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











