因为锁粒度从“段”细化到“桶”,使并发度上限从固定16提升至数组长度,不同桶操作完全无锁竞争;JDK 8采用CAS无锁插入+桶头节点synchronized锁,读操作则依赖volatile字段保证可见性,全程无锁。

锁粒度从“段”到“桶”,为什么更细就更高效?
Java 8 彻底移除了 JDK 7 的 Segment 分段锁,改用以数组每个桶(table[i])的头节点为锁对象的策略。这意味着:只有对同一个桶执行写操作(如 put、remove)时才会竞争锁,不同桶之间完全无干扰。
这种优化的关键在于降低锁冲突概率。比如 16 个线程同时往 map 插入数据,若哈希均匀,平均每个桶只被 1 个线程访问,绝大多数写操作可并行执行;而分段锁下,即使散列再好,16 段锁也意味着最多 16 路并发,且段内所有桶共享一把锁,锁内链表操作仍可能串行化。
实际加锁方式是:先用 CAS 尝试无锁插入;失败后(比如桶非空或 CAS 竞争),再对头节点(Node)加 synchronized,仅锁定该桶的迁移或修改逻辑。
读操作为何能完全不加锁?
ConcurrentHashMap 的 get 操作全程无锁,依赖两个关键设计:
立即学习“Java免费学习笔记(深入)”;
- volatile Node[] table:保证数组引用的可见性,新表发布后所有线程立即看到
- volatile Node.next 和 Node.val:链表节点字段均为 volatile,配合 happens-before 规则,确保读线程能看到其他线程已安全写入的值
- 即使某次 get 时桶正在被迁移,也会遇到
ForwardingNode,自动跳转到新表继续查找,整个过程对调用方透明
扩容不是“等满了再搬”,而是边用边迁
扩容是渐进式、协作式的,没有全局停顿:
- 触发条件有三类:元素总数超阈值(
sizeCtl)、链表长度≥8 且数组长度<64(此时优先扩容而非树化)、putAll预估容量不足 - 扩容时新建两倍长的新数组
nextTable,并通过transferIndex划分迁移区间,多个线程可各自负责一段桶的迁移 - 每个桶迁移前先加锁(锁头节点),把旧桶中节点按新容量重哈希后拆成两组(低位/高位),分别插入新表对应位置
- 迁移完的桶会被设为
ForwardingNode,后续所有对该桶的读写操作都会转向新表,避免重复迁移
sizeCtl 变量:一个整数承载多种状态
sizeCtl 是控制初始化、扩容和并发协调的核心标志位,其取值含义如下:
-
正数:表示下一次扩容的阈值(即
capacity × loadFactor) - 0:表示 table 尚未初始化
- -1:表示 table 正在初始化中
- 小于 -1:表示有线程正在扩容,高 16 位为扩容标识,低 16 位记录当前参与扩容的线程数
所有状态变更都通过 CAS 原子更新,是整个扩容流程协同的基础。


















