ConcurrentHashMap的initTable方法为何要两次检查table是否为空?
ConcurrentHashMap initTable() 二次校验问题解答
相关源码回顾
private final Node<K,V>[] initTable() { Node<K,V>[] tab; int sc; while ((tab = table) == null || tab.length == 0) { if ((sc = sizeCtl) < 0) Thread.yield(); // lost initialization race; just spin else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { try { if ((tab = table) == null || tab.length == 0) { int n = (sc > 0) ? sc : DEFAULT_CAPACITY; @SuppressWarnings("unchecked") Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; table = tab = nt; sc = n - (n >>> 2); } } finally { sizeCtl = sc; } break; } } return tab; }
关于你的猜测的正确性
你的猜测不成立。ConcurrentHashMap的table成员变量是被volatile修饰的,根据Java内存模型的volatile内存语义:
对volatile变量的写操作,之前的所有普通写操作(包括新Node数组的创建、赋值操作)都不会被重排序到volatile写之后。代码中sizeCtl = sc的赋值操作,执行顺序在volatile写table = nt之后,因此不可能被JVM重排序到数组创建操作之前。
第二次校验的适用场景
这是典型的**双重检查锁定(DCL)**实现,核心作用是避免多线程并发竞争时的重复初始化问题,对应的经典并发时序如下:
- 初始状态:
ConcurrentHashMap刚完成构造,table = null,sizeCtl = 16(假设指定初始容量为16) - 线程T1执行
initTable(),通过第一次while校验(tab = table) == null,正准备读取sizeCtl时CPU时间片耗尽,被系统挂起 - 线程T2进入
initTable(),顺利通过第一次校验,读取sizeCtl = 16,CAS操作成功将sizeCtl改为-1抢到初始化权限 - T2完成数组初始化,将
table赋值为长度16的Node数组,计算得到新的阈值sc = 12,finally块中将sizeCtl更新为12,初始化完成退出 - 此时T1被唤醒,继续执行读取到当前
sizeCtl = 12,CAS操作成功将sizeCtl从12改为-1,抢到权限进入try块 - 如果没有第二次校验,T1会重新创建新的Node数组,覆盖已经初始化完成、甚至已经存入了数据的
table,造成数据丢失。有了第二次校验,T1会读取到最新的table已经不为空,直接跳过初始化逻辑,不会触发重复创建。
两次校验的分工
- 第一次校验:快速路径,绝大多数场景下数组已经初始化完成,不需要进入循环抢CAS,直接返回即可,提升调用性能
- 第二次校验:安全兜底,确保只有第一个真正抢到初始化权限、且此时数组确实未初始化的线程,才会执行初始化操作,从根源避免重复初始化的线程安全问题。
内容的提问来源于stack exchange,提问作者peter Char
相关产品推荐
相关产品推荐

