ConcurrentHashMap 扩容机制:高并发下的安全扩容实现

一、前言

在上一篇文章中,我们拆解了 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 的扩容机制体现了高并发编程的三大核心思想,保证了扩容的高效性和安全性:

  1. **协作式扩容:**将扩容任务分散到多个线程,避免单线程扩容的性能瓶颈,提升扩容效率;

  2. **分段迁移:**通过 transferIndex 和步长实现区间划分,减少多线程对同一桶的竞争;

  3. **读写无感:**通过 ForwardingNode 实现扩容期间的读写路由,保证操作不阻塞且数据不丢失;

  4. **锁粒度最小化:**仅对迁移中的桶加锁,而非全表加锁,最大限度降低对并发性能的影响。

八、结语

本文深度拆解了 ConcurrentHashMap 的扩容机制,其 "单线程初始化、多线程协作迁移、ForwardingNode 路由" 的设计,既保证了高并发下的扩容安全,又通过协作式设计和分段迁移实现了性能最大化。

相关推荐
ha_lydms2 小时前
6、Spark 函数_u/v/w/x/y/z
java·大数据·python·spark·数据处理·dataworks·spark 函数
这我可不懂2 小时前
谈谈mcp协议的实现
开发语言·qt·哈希算法
胡闹542 小时前
MyBatis-Plus 更新字段为 null 为何失效?
java·数据库·mybatis
糕......2 小时前
JDK安装与Java开发环境配置全攻略
java·开发语言·网络·学习
日日行不惧千万里2 小时前
Java中Lambda Stream详解
java·开发语言·python
ss2732 小时前
线程池关闭:shutdown与shutdownNow的区别
java
2401_841495642 小时前
【LeetCode刷题】零钱兑换
数据结构·python·算法·leetcode·动态规划·数组·时间复杂度
趁月色小酌***2 小时前
JAVA 知识点总结4
java·开发语言
C雨后彩虹2 小时前
ConcurrentHashMap 源码逐行拆解:put/get 方法的并发安全执行流程
java·算法·哈希算法·集合·hashmap