ConcurrentHashMap 采用桶级细粒度锁替代全局锁,JDK 1.8 以 CAS + synchronized 锁头节点、volatile 变量保障可见性,实现高并发读写;避免 HashMap 死循环与 HashTable 低效互斥问题。

ConcurrentHashMap 的核心原理,是用“分段锁”到“桶级细粒度锁”的演进,解决 HashMap 线程不安全、HashTable 全局锁低效的两难问题。它不是靠一把大锁拦住所有线程,而是让读写操作尽可能并行——读几乎不加锁,写只锁住当前操作的那个桶(Node 链表头),扩容还能多线程协作。JDK 1.8 是分水岭:彻底抛弃 Segment,改用 CAS + synchronized 锁头节点,配合 volatile 变量保障可见性,性能和可维护性都大幅提升。
为什么不用 HashMap 或 HashTable?
HashMap 在多线程 put 时可能引发链表成环,导致死循环(CPU 占满);HashTable 虽线程安全,但所有方法都 synchronized(this),读写互斥,吞吐量随并发线程数增加而急剧下降。ConcurrentHashMap 直接绕开这两个坑:既避免了结构破坏风险,又把锁范围从“整个表”缩小到“一个桶”,甚至多数读操作完全无锁。
JDK 1.8 的关键结构与机制
底层仍是数组 + 链表/红黑树,但关键设计全为并发服务:
- volatile Node[] table:主数组声明为 volatile,保证数组引用的可见性;每个 Node 的 val 和 next 也是 volatile,确保链表遍历时能读到最新值
- 无锁初始化:首次 put 时通过 CAS 控制 initTable(),仅一个线程成功初始化,其余自旋等待
- 桶级加锁:插入时先 CAS 尝试无锁写入;失败则对目标桶的头节点(f)加 synchronized 锁,只阻塞同桶写入,不同桶完全不受影响
- 协助扩容:当发现 ForwardingNode(hash == -1),说明正在扩容,当前线程会主动帮着迁移部分数据(stride 默认 16),而不是干等
- 计数分离:size() 不遍历全表,而是用类似 LongAdder 的 CounterCell 数组分段累加,减少竞争
put 操作的典型流程(简化)
以 JDK 1.8 源码逻辑为主线:
立即学习“Java免费学习笔记(深入)”;
- 检查 key/value 是否为 null(直接抛 NPE,不允许)
- 计算 hash 值(spread(key.hashCode()),高位参与扰动)
- 若 table 为空,先 CAS 初始化
- 定位桶位置 i = (n-1) & hash
- 桶为空 → CAS 插入新 Node
- 桶为 ForwardingNode → 帮忙扩容
- 桶有节点 → synchronized(f) 后遍历链表或树,处理重复 key 或追加节点;长度超 8 且 table ≥ 64 时触发树化
- 最后调用 addCount() 更新元素总数
读操作为何几乎不加锁?
因为三个保障同时存在:
- Node 的 key、hash 是 final,不可变
- val 和 next 是 volatile,写后立即对其他线程可见
- 读前不做同步,但若读到正在迁移的桶(ForwardingNode),会主动跳转到新表重读——即“重试读”,保证结果一致性
这种设计让 get() 在绝大多数场景下是纯内存读取,没有锁开销,也无 CAS 失败重试成本。
基本上就这些。它不是靠魔法,而是把并发控制拆解到最细的执行单元,再用内存模型和原子指令兜底。理解它,关键不在背代码,而在看清“哪些操作必须串行”“哪些可以天然并行”“哪些靠 volatile/CAS 就够了”。










