一、前言
在上一篇文章中,我们拆解了 ConcurrentHashMap 的 put 和 get 方法源码,明确了其核心的并发安全存取逻辑。而在高并发场景下,扩容操作的实现难度远高于普通 Map------ 既要保证扩容过程中的数据一致性,又要避免单线程扩容带来的性能瓶颈。
本文将深度解析 JDK1.8 ConcurrentHashMap 的扩容机制,从触发条件 、 核心流程 、 多线程协作到并发安全保障,带你吃透高并发场景下的扩容实现逻辑。
二、扩容的核心触发条件
ConcurrentHashMap 的扩容并非单一条件触发,而是结合元素数量阈值 和树化前置检查两种场景,保证扩容操作的合理性和及时性。
1. 元素数量触发扩容
这是最核心的触发条件,在 addCount 方法中完成判断:
-
当 ConcurrentHashMap 的元素总数 size 超过扩容阈值 sizeCtl 时,触发扩容;
-
扩容阈值 sizeCtl 的初始值为 0.75 * 初始容量 (默认初始容量 16,故初始阈值为 12);
-
每次扩容后,新阈值为 0.75 * 新容量 (容量翻倍,故阈值也翻倍)。
2. 树化前置的容量校验触发扩容
在 treeifyBin 方法(链表转红黑树)中,会先校验数组容量:
-
若数组容量 < MINTREEIFYCAPACITY(64) ,则优先触发扩容(容量翻倍),而非直接将链表转为红黑树;
-
仅当数组容量≥64 且链表长度≥8 时,才会执行树化操作,避免小容量数组因过度树化导致内存浪费。
三、扩容的核心成员变量与标识
ConcurrentHashMap 通过多个核心变量标识扩容状态,保证多线程下的协作有序,关键变量如下:
java
// 扩容中的临时新数组,存储迁移后的元素
private transient volatile Node<K,V>[] nextTable;
// 扩容控制标识,扩容时的取值规则为:resizeStamp(n) << RESIZE_STAMP_SHIFT + 线程数
// resizeStamp:根据旧容量生成的扩容标记;RESIZE_STAMP_SHIFT:固定为16
private transient volatile int sizeCtl;
// 扩容中转节点,hash值固定为MOVED(-1),用于标识原数组中正在迁移的桶
static final int MOVED = -1;
// 扩容时的步长,默认16,用于多线程分段迁移元素
private static final int MIN_TRANSFER_STRIDE = 16;
其中 resizeStamp 的核心作用是为不同容量的数组生成唯一标识,避免扩容过程中因容量变化导致的状态混乱,其计算逻辑为:
java
static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
四、扩容的核心流程:transfer 方法
ConcurrentHashMap 的扩容核心逻辑在 transfer 方法中实现,整体流程分为单线程初始化新数组和多线程协作迁移元素两个阶段,同时通过 ForwardingNode 保证扩容期间的读写安全。
1. transfer 方法完整源码(核心片段)
java
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
// 计算每个线程的迁移步长,CPU核心数越多,步长越大(最少为16)
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE;
// 阶段1:单线程初始化新数组
if (nextTab == null) {
try {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]; // 新容量为旧容量的2倍
nextTab = nt;
} catch (Throwable ex) {
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;
transferIndex = n; // 迁移索引,从旧数组尾部开始迁移
}
int nextn = nextTab.length;
// 创建扩容中转节点,用于标识原数组中已迁移的桶
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true; // 标记是否可以推进迁移索引
boolean finishing = false; // 标记扩容是否完成
// 自旋迁移,i为当前迁移的桶索引,bound为当前线程的迁移区间下界
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
// 阶段2:分配当前线程的迁移区间
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing)
advance = false;
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
// CAS抢占迁移区间,从transferIndex向前分配stride长度的区间
else if (U.compareAndSwapInt(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
// 阶段3:处理单个桶的元素迁移
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
// 扩容完成,更新状态
if (finishing) {
nextTable = null;
table = nextTab; // 原数组引用指向新数组
sizeCtl = (n << 1) - (n << 1 >>> 2); // 计算新阈值(0.75*新容量)
return;
}
// CAS减少扩容线程数,标记当前线程退出扩容
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
i = n; // 重新遍历,检查是否有遗漏的桶
}
}
// 桶为空,直接放入ForwardingNode标识已迁移
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
// 桶已迁移(是ForwardingNode),跳过
else if ((fh = f.hash) == MOVED)
advance = true;
// 桶有元素,加锁迁移
else {
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn; // 低位链表、高位链表(新数组的两个桶)
if (fh >= 0) { // 普通链表结构
int runBit = fh & n; // 确定元素在新数组的位置(0:低位桶i;1:高位桶i+n)
Node<K,V> lastRun = f;
// 遍历链表,找到最后一个连续同位置的节点,减少迁移次数
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
if (runBit == 0) {
ln = lastRun;
hn = null;
} else {
hn = lastRun;
ln = null;
}
// 拆分链表为低位和高位
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.value;
if ((ph & n) == 0)
ln = new Node<K,V>(ph, pk, pv, ln);
else
hn = new Node<K,V>(ph, pk, pv, hn);
}
// 放入新数组对应的桶
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
// 原桶放入ForwardingNode,标识已迁移
setTabAt(tab, i, fwd);
advance = true;
}
else if (f instanceof TreeBin) { // 红黑树结构
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> lo = null, loTail = null;
TreeNode<K,V> hi = null, hiTail = null;
int lc = 0, hc = 0;
// 拆分红黑树为低位和高位
for (Node<K,V> e = t.first; e != null; e = e.next) {
int h = e.hash;
TreeNode<K,V> p = new TreeNode<K,V>(h, e.key, e.value, null, null);
if ((h & n) == 0) {
if ((p.prev = loTail) == null)
lo = p;
else
loTail.next = p;
loTail = p;
++lc;
} else {
if ((p.prev = hiTail) == null)
hi = p;
else
hiTail.next = p;
hiTail = p;
++hc;
}
}
// 决定是否转回链表
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
(hc != 0) ? new TreeBin<K,V>(lo) : t;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin<K,V>(hi) : t;
// 放入新数组
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
// 原桶标记为已迁移
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}
2. transfer 方法逐阶段拆解
阶段 1:单线程初始化新数组
-
只有第一个触发扩容的线程会执行新数组初始化,新数组容量为旧数组的2 倍;
-
初始化完成后,将新数组引用赋值给 nextTable ,并设置 transferIndex 为旧数组长度(从尾部开始迁移)。
阶段 2:多线程抢占迁移区间
-
每个线程通过 CAS 抢占 transferIndex ,分配一段连续的桶区间(步长 stride ,默认 16)进行迁移;
-
线程从区间的尾部向头部迁移,避免多线程操作同一桶,减少竞争。
阶段 3:单个桶的元素迁移
这是扩容的核心逻辑,对每个桶的处理分为 4 种情况,且迁移时会对桶首节点加 synchronized 锁,保证数据一致性:
**1.桶为空:**直接通过 CAS 将桶设置为 ForwardingNode ,标识已迁移;
**2.桶已迁移:**检测到桶是 ForwardingNode ,直接跳过;
3.普通链表桶:
-
通过 hash & 旧容量 判断元素在新数组的位置(结果为 0 则放入新数组低位桶 i ,为 1 则放入高位桶 i+n );
-
将原链表拆分为 低位链表 和 高位链表 ,分别放入新数组对应的桶;
-
原桶设置为 ForwardingNode ,完成迁移。
4.红黑树桶:
-
按同样规则将红黑树拆分为低位树和高位树;
-
若拆分后的树节点数≤6,则转回链表,平衡性能与内存;
-
原桶设置为 ForwardingNode ,完成迁移。
阶段 4:扩容完成的状态更新
-
所有线程完成迁移后,最后一个退出的线程会执行收尾操作:将 table 引用指向新数组,清空 nextTable ,并计算新的扩容阈值;
-
通过 sizeCtl 的 CAS 操作保证收尾操作的原子性,避免多线程重复执行。
五、扩容期间的读写协作与并发安全保障
ConcurrentHashMap 在扩容期间通过 ForwardingNode 实现读写操作的无缝协作,保证数据一致性和操作不阻塞,核心逻辑如下:
1. 写操作(put)的协作逻辑
当 put 操作遇到 ForwardingNode 时:
-
会先调用 helpTransfer 方法,协助当前正在进行的扩容操作;
-
扩容完成后,再将新元素插入到新数组中,避免写入旧数组导致数据丢失。
2. 读操作(get)的协作逻辑
当 get 操作遇到 ForwardingNode 时:
-
会调用 ForwardingNode 的 find 方法,直接到 nextTable (新数组)中查询元素;
-
无需等待扩容完成,保证读操作的无锁化和高效性,同时获取最新数据。
3. 扩容线程的协作与退出
-
每个线程完成自身区间的迁移后,会通过 CAS 将 sizeCtl 的值减 1(表示扩容线程数减少);
-
当 sizeCtl 的值降至 resizeStamp(n) << 16 + 1 时,说明只剩最后一个线程,该线程会执行扩容收尾工作。
六、ConcurrentHashMap 与 HashMap 扩容的核心差异
为了更清晰理解高并发扩容的设计优势,我们对比其与 HashMap 扩容的核心区别:
| 对比维度 | HashMap 扩容 | ConcurrentHashMap 扩容 |
|---|---|---|
| 执行线程 | 单线程执行,扩容时阻塞其他操作 | 多线程协作,支持线程协助迁移,无阻塞 |
| 扩容标识 | 无专门标识,依赖 size 和 threshold | 通过sizeCtl 和 ForwardingNode标识状态 |
| 读写兼容性 | 扩容时写操作阻塞,读可能获取旧数据 | 扩容时读写不阻塞,读自动路由到新数组 |
| 迁移方式 | 从头至尾逐个迁移,无区间划分 | 多线程分段抢占区间,从尾至头迁移 |
| 数据拆分逻辑 | 仅计算新索引,无链表拆分优化 | 按 hash & 旧容量拆分链表 / 红黑树,减少遍历 |
七、扩容机制的核心设计思想
ConcurrentHashMap 的扩容机制体现了高并发编程的三大核心思想,保证了扩容的高效性和安全性:
-
**协作式扩容:**将扩容任务分散到多个线程,避免单线程扩容的性能瓶颈,提升扩容效率;
-
**分段迁移:**通过 transferIndex 和步长实现区间划分,减少多线程对同一桶的竞争;
-
**读写无感:**通过 ForwardingNode 实现扩容期间的读写路由,保证操作不阻塞且数据不丢失;
-
**锁粒度最小化:**仅对迁移中的桶加锁,而非全表加锁,最大限度降低对并发性能的影响。
八、结语
本文深度拆解了 ConcurrentHashMap 的扩容机制,其 "单线程初始化、多线程协作迁移、ForwardingNode 路由" 的设计,既保证了高并发下的扩容安全,又通过协作式设计和分段迁移实现了性能最大化。