inittable 通过 volatile sizectl 与 cas 实现无锁单例初始化:仅一个线程能将 sizectl 由非负值设为 -1 以获得权限,成功者创建 table 并设阈值,失败者自旋等待;volatile table 与 volatile 读确保可见性,避免半成品访问。

initTable 方法通过 CAS 保证单例初始化,核心在于用一个 volatile 修饰的 sizeCtl 字段作为状态控制器,只允许一个线程成功将它从初始值(如 0 或负数)设为 -1,从而获得初始化权限。
sizeCtl 是初始化的“门禁开关”
ConcurrentHashMap 的 sizeCtl 是一个 volatile int 字段,初始化前它的值通常为 0(无参构造)或用户计算出的容量(有参构造)。当首次调用 put 触发初始化时,initTable 会执行以下关键逻辑:
- 循环检测:若
table == null且sizeCtl >= 0,说明尚未开始初始化; - CAS 尝试抢锁:调用
U.compareAndSwapInt(this, SIZECTL, sc, -1),把sizeCtl从当前值sc改为 -1; - 仅有一个线程能成功:因为 CAS 是原子操作,多线程竞争下最多一个线程返回 true;
- 失败线程让出:其余线程发现
sizeCtl == -1,就主动让出 CPU(短暂自旋或 yield),等待初始化完成再重试。
初始化过程不加锁,但靠 volatile + CAS 串行化
一旦某个线程 CAS 成功,它就独占初始化资格:
- 此时
sizeCtl = -1已对所有线程可见(volatile 保证); - 该线程创建新 Node 数组(默认长度 16),赋值给
table字段; - 最后设置
sizeCtl为扩容阈值(如 12 = 16 × 0.75),表示初始化完毕; - 其他线程在下一轮循环中看到
table != null,直接退出初始化流程。
为什么不用 synchronized?
用 CAS + volatile 替代锁,是为了避免初始化这个一次性动作引入重量级同步开销:
- synchronized 会触发操作系统互斥量,带来上下文切换成本;
- CAS 在多数情况下是 CPU 指令级原子操作,无锁、无阻塞、响应更快;
- 即使 CAS 失败,线程也只是轻量自旋,不挂起,适合短时临界区(如数组分配)。
失败线程不会重复初始化,也不会读到半成品 table
关键保障来自两方面:
-
table字段被声明为transient volatile Node<k>[] table</k>,写入新数组后对其他线程立即可见; - 所有读取
table的路径(如tabAt)都使用Unsafe.getObjectVolatile,确保拿到最新引用; - 没有线程能在
table赋值完成前看到非 null 值,也就不会基于未初始化的数组做后续操作。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











