ConcurrentHashMap 的扩容机制

715 阅读3分钟
1. 数据结构基础(JDK1.8)
  • 底层结构:由Node<K,V>[] table数组、链表、红黑树组成,数组大小始终为 2 的幂次(如 16、32)。
  • 并发控制:摒弃 JDK1.7 的分段锁(Segment),改用CAS + synchronized,锁粒度更小(锁定链表或红黑树的头节点)。
2. 扩容触发条件
  • 阈值计算:初始容量为 16,负载因子默认为 0.75,扩容阈值为容量×负载因子(如 16×0.75=12)。当元素数量超过阈值时触发扩容。

  • sizeCtl 参数

    • sizeCtl=-1:表示数组正在初始化;
    • sizeCtl> 0:表示初始化或扩容的阈值;
    • sizeCtl=-N:表示有 N-1 个线程正在参与扩容(N 为扩容线程数)。
3. 扩容核心过程(重点)

假设原数组大小为oldCap,扩容后为newCap=oldCap×2,过程如下:

(1)触发扩容

当向 map 中添加元素时,通过addCount()方法检查元素数量是否超过阈值,若超过则调用transfer()方法启动扩容。

(2)分段迁移与线程协作
  • 分割任务:将原数组分为多个迁移段(每个段大小为oldCap/16),每个线程处理一段。
  • ForwardingNode 标记:在原数组位置放置ForwardingNode,表示该位置已被迁移,其他线程访问时会被引导到新数组。
(3)数据迁移逻辑

对每个节点e

  • enull:新数组对应位置直接置null
  • eForwardingNode:跳过(已被其他线程处理);
  • e为链表节点:遍历链表,重新计算哈希值,根据新数组大小决定节点在新数组中的位置(因newCap=oldCap×2,哈希值的高位决定是否迁移到高位区间);
  • e为红黑树节点:先转化为链表再迁移,若迁移后链表长度≥6,重新转为红黑树。
(4)扩容完成与状态更新

当所有段迁移完成后,将table指向新数组,更新sizeCtl为新的阈值,原数组被垃圾回收。

4. 并发安全与优化策略
  • 并发控制

    • 迁移过程中,通过CAS更新sizeCtl控制线程数,避免多线程冲突;
    • 对单个节点的操作使用synchronized锁定,确保数据迁移时的原子性。
  • 性能优化

    • 分段扩容:多个线程并行处理不同段,减少单线程负载;
    • 懒迁移:允许线程在处理完自己的段后,协助其他线程迁移,避免扩容阻塞;
    • 哈希高位利用:扩容时通过哈希值高位直接定位新位置,无需重新计算哈希,提升效率。
5. 与 HashMap 扩容的对比
  • HashMap 扩容:单线程操作,扩容时会复制所有元素,期间无法读写;
  • ConcurrentHashMap 扩容:多线程并行迁移,迁移过程中允许读写(读操作会被ForwardingNode引导到新数组),无全局锁阻塞,并发性能更优。
6. 扩容流程示意图
  • 扩容前数据结构image.png
  • 扩容过程:高低位链表拆分image.png
  • 扩容后的数据结构image.png
  • 扩容核心流程时序图image.png
7. 核心原理总结
  1. 触发条件:元素数量超过 容量 × 负载因子(默认 16×0.75=12)

  2. 扩容规则:容量翻倍(16→32→64...)

  3. 位置计算

    • 若 hash & oldCap == 0:新位置 = 原位置
    • 若 hash & oldCap != 0:新位置 = 原位置 + oldCap
  4. 效率优化

    • 无需重新计算哈希值,仅通过位运算定位
    • 一次遍历将链表拆分为高低位两部分,减少操作次数

这种设计使得 ConcurrentHashMap 在扩容时的性能显著优于传统哈希表,尤其在处理大规模数据时优势更加明显。