因为锁粒度从“段”细化到“桶”,使并发度上限从固定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 代码添加注释、修改注释、审查注释、整理注释格式,或要求按照代码注释规范处理 Java 文件时,使用本技能。本技能用于统一生成和维护 Java 类注释模板、按 Git 对比 release 判断版本号、补充每行代码上方中文块注释、调整注释空行规则,并保持 Java 文件 UTF-8 无 BOM 编码。典型触发场景包括:补充类注释、更新 @version、检查 @author 来源、整理行尾注释、把注释放到代码上一行、检查注释是否符合规范、构建前检查 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 原子更新,是整个扩容流程协同的基础。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










