上周团队在优化一个高并发配置中心时针对缓存选型爆发了一场激烈争论。一方主张“直接用 ConcurrentHashMap线程安全性能好JDK 自带不用白不用。”另一方反驳“你们真的了解它的扩容机制吗在高并发写入场景下多个线程同时触发扩容会不会导致 CPU 飙升甚至死循环”争论的焦点集中在一段看似简单的代码上ConcurrentHashMapString, Config configCache new ConcurrentHashMap(); // 高并发环境下频繁 put executor.submit(() - { for (int i 0; i 10000; i) { configCache.put(key_ Thread.currentThread().getId() _ i, new Config()); } });起初大家都认为这是“标准写法”直到压测时发现在 32 核机器上随着线程数增加到 64CPU 使用率从 30% 飙升至 95%且吞吐量不升反降。日志中未见 OOM但 GC 频率正常线程堆栈显示大量线程卡在transfer方法。这显然不是预期的“无锁高性能”。于是我们决定深入 ConcurrentHashMap 的扩容源码揭开其并发控制机制的真实面貌。需求约束为什么不能用 HashMap 或 Hashtable在开始源码分析前先明确我们的业务场景约束配置中心需支持每秒上万次读写配置更新频率高存在热点 key 频繁修改不允许因并发问题导致数据丢失或脏读系统运行在 JDK 17 环境但需兼容 JDK 8 行为。HashMap 在多线程下会因 rehash 导致链表成环引发死循环Hashtable 使用全表锁并发度极低。因此 ConcurrentHashMap 成为唯一合理选择——但前提是我们必须正确使用它。架构设计ConcurrentHashMap 的并发哲学ConcurrentHashMap 并非“完全无锁”而是通过分段锁 CAS synchronized 精细化控制实现高并发。其核心设计思想是桶级别锁每个哈希桶bin独立加锁避免全局锁竞争CAS 无锁读/写在桶为空或仅有一个节点时优先使用 CAS 插入协助扩容机制当一个线程触发扩容时其他线程可协助迁移数据避免单点瓶颈sizeCtl 状态机通过 volatile 变量控制初始化、扩容、终止等状态。然而很多人误以为“只要用了 ConcurrentHashMap就可以随意高并发 put”忽略了扩容过程中的协作成本。关键代码走读transfer 方法如何工作我们定位到问题的核心transfer方法。这是扩容时数据迁移的关键逻辑位于java.util.concurrent.ConcurrentHashMap类中。错误直觉扩容是串行的很多人以为扩容只能由一个线程完成其他线程必须等待。实际上ConcurrentHashMap 允许多个线程并行迁移不同段的数据。private final void transfer(NodeK,V[] tab, NodeK,V[] nextTab) { int n tab.length, stride; if ((stride (NCPU 1) ? (n 3) / NCPU : n) MIN_TRANSFER_STRIDE) stride MIN_TRANSFER_STRIDE; // 每个线程最少处理 16 个桶 if (nextTab null) { // 初始化新表 try { SuppressWarnings(unchecked) NodeK,V[] nt (NodeK,V[])new Node?,?[n 1]; nextTab nt; } catch (Throwable ex) { // 内存不足 sizeCtl Integer.MAX_VALUE; return; } nextTable nextTab; transferIndex n; } int bound 0; ForwardingNodeK,V fwd new ForwardingNode(nextTab); boolean advance true; boolean finishing false; // 是否完成迁移 for (int i 0, bound 0; ; i nextIndex) { // 获取当前要处理的桶范围 while (advance) { nextIndex transferIndex - stride; if (nextIndex (bound nextIndex - stride)) { nextIndex transferIndex 0; advance false; } } if (i 0 || i n || i n nextn) { // 当前线程无任务可做 if (finishing) { nextTable null; table nextTab; sizeCtl (n 1) - (n 1); // 更新阈值 return; } // 尝试减少工作线程数 if (U.compareAndSwapInt(this, SIZECTL, sc sizeCtl, sc - 1)) { if ((sc - 2) ! resizeStamp(n) RESIZE_STAMP_SHIFT) return; finishing advance true; i n; // 重新检查一遍 } } else if (U.compareAndSwapInt(this, TRANSFERINDEX, nextIndex, bound nextIndex - stride)) { // 成功抢到一段任务开始迁移 for (int j i; j bound; j) { // 迁移单个桶 migrateBucket(tab, nextTab, j, fwd); } advance true; } } }关键机制解析stride 划分任务每个线程负责迁移stride个桶默认至少 16 个避免任务过细transferIndex 原子递减通过 CAS 分配任务区间确保不重复不遗漏ForwardingNode 占位原桶被迁移后插入ForwardingNode告诉后续 get/put 操作“去新表查”协助扩容任何线程在 put 时发现正在扩容都会尝试调用helpTransfer协助迁移。为什么压测时 CPU 飙升根本原因在于线程数远超过 CPU 核心数导致大量线程争抢 transferIndexCAS 失败率高自旋消耗 CPU。此外如果初始容量设置过小如默认 16在短时间内插入大量数据会频繁触发扩容进一步加剧竞争。复盘如何正确使用 ConcurrentHashMap经过源码分析和压测验证我们总结出以下实践建议预估容量避免频繁扩容// 错误默认容量 16插入 10w 数据会扩容多次 new ConcurrentHashMap(); // 正确预估大小减少扩容次数 new ConcurrentHashMap(100000);控制并发写入线程数线程数建议不超过 CPU 核心数的 2 倍若必须高并发考虑使用computeIfAbsent或merge减少 put 竞争。避免热点 key 集中写入热点 key 会导致同一桶频繁操作即使有锁分离也可能成为瓶颈可考虑本地缓存 异步同步策略。监控 sizeCtl 和 transferIndex通过反射或 JMX 监控扩容状态及时发现异常在日志中打印table.length观察扩容频率。JDK 版本差异注意JDK 8 使用分段锁 CASJDK 9 引入synchronized替代部分ReentrantLock性能更优建议使用最新 LTS 版本。最终我们将配置中心改造为使用ConcurrentHashMap但预分配容量写入操作通过computeIfAbsent保证原子性添加本地 Caffeine 缓存作为一级缓存减少对 ConcurrentHashMap 的直接访问压测结果显示64 线程下 CPU 使用率降至 45%吞吐量提升 3 倍。技术补丁包ConcurrentHashMap 扩容机制原理通过 transferIndex 原子分配迁移任务多个线程可并行迁移不同桶迁移完成后插入 ForwardingNode 引导查询到新表。 设计动机避免单点扩容瓶颈提升高并发写入性能。 边界条件线程数过多会导致 CAS 竞争加剧反而降低性能初始容量过小会频繁触发扩容。 落地建议预估数据量预分配容量控制并发线程数监控扩容频率。CAS 与 synchronized 的协作原理桶为空时使用 CAS 插入桶非空且需扩容时使用 synchronized 锁住头节点。 设计动机最小化锁粒度兼顾性能与正确性。 边界条件CAS 在高度竞争下失败率高可能退化为锁竞争synchronized 在 JDK 15 有显著优化。 落地建议优先使用 CAS 场景避免人为制造热点 key。ForwardingNode 的作用原理扩容期间原桶被替换为 ForwardingNode其 hash 值为 -1指向新表。 设计动机实现无锁读的连续性get 操作无需等待扩容完成。 边界条件put 操作遇到 ForwardingNode 会触发 helpTransfer可能增加延迟。 落地建议理解其存在意义避免误判为“空桶”或“异常节点”。sizeCtl 状态机原理volatile 变量控制初始化-1、正常阈值、扩容负值表示扩容线程数等状态。 设计动机统一协调初始化、扩容、终止等生命周期事件。 边界条件多线程同时初始化或扩容时依赖 CAS 保证原子性。 落地建议可通过反射查看其值辅助排查问题但勿手动修改。协助扩容helpTransfer原理任何线程在 put/get 时发现正在扩容都会尝试协助迁移数据。 设计动机充分利用系统资源加快扩容速度。 边界条件协助线程仍需竞争 transferIndex可能成为新瓶颈。 落地建议在高并发场景下合理设置初始容量减少扩容触发频率。