JDK8 ConcurrentHashMap 底层数据结构详解

24 阅读9分钟

1. 整体架构与核心属性

image.png

JDK8 中的 ConcurrentHashMap 通过一系列精心设计的核心属性实现高效并发,这些属性控制着数据结构的初始化、扩容、并发控制及数据存储:

1.1. 数据存储核心:table 数组

transient volatile Node<K,V>[] table;
  • 功能

    • 存储键值对的核心数组,初始长度为 16,扩容时长度翻倍
    • 数组元素类型为 Node,可能是普通链表节点、红黑树节点或扩容节点
  • 关键点

    • volatile 修饰确保可见性,一个线程对数组的修改对其他线程立即可见
    • 数组长度始终为 2 的幂,通过位运算替代取模,提升哈希效率

1.2. 扩容辅助:nextTable

    private transient volatile Node<K,V>[] nextTable;
  • 功能

    • 扩容时用于存储新数组,长度为原数组的 2 倍
    • 扩容完成后,nextTable 被置为 nulltable 指向新数组
  • 关键点

    • 扩容采用 多线程协作,通过 ForwardingNode 标记已迁移的桶
    • 读操作遇到 ForwardingNode 时,会自动转发到 nextTable 进行查询

1.3. 全局控制:sizeCtl

    private transient volatile int sizeCtl;
  • 功能

    • 控制 table 的初始化和扩容操作,其值的不同状态代表不同含义:

      • -1:表示 table 正在初始化
      • 负数且非 -1:表示正在扩容,-N 表示有 N-1 个线程正在参与扩容
      • 0:默认值,表示 table 尚未初始化
      • 正数:初始化阈值或下次扩容的阈值(初始为 12,即 16×0.75)
  • 关键点

    • 通过 CAS 操作修改 sizeCtl,确保多线程环境下的原子性
    • 扩容时通过 resizeStamp(n) << RESIZE_STAMP_SHIFT + 2 生成特殊标记值

1.4. 阈值控制:threshold

    private final int threshold;
  • 功能

    • 链表转红黑树的阈值:当链表长度达到 8 且数组长度≥64 时,转换为红黑树
    • 红黑树转链表的阈值:当红黑树节点数减少到 6 时,转回链表
  • 关键点

    • 与 sizeCtl 的区别:threshold 控制数据结构转换,sizeCtl 控制扩容

1.5. 修改计数:modCount

    private transient volatile int modCount;
  • 功能

    • 记录 ConcurrentHashMap 的结构修改次数(如插入、删除)
    • 用于支持弱一致性迭代器,确保迭代过程中不会抛出 ConcurrentModificationException
  • 关键点

    • 弱一致性保证:迭代器可能反映最新修改,也可能反映创建时的状态

1.6. 元素计数:baseCount 与 CounterCell

    private transient volatile long baseCount;
    private transient volatile CounterCell[] counterCells;
  • 功能

    • baseCount:无竞争时,直接通过 CAS 更新该值统计元素数量
    • CounterCell[] :竞争激烈时,各线程在不同的 CounterCell 中计数,最后合并结果
  • 关键点

    • 类似 LongAdder 的分段计数思想,减少 CAS 冲突
    • 元素总数 = baseCount + 所有 CounterCell 的值之和

1.7. 哈希扰动:HASH_BITS

    static final int HASH_BITS = 0x7fffffff;
  • 功能

    • 用于计算哈希值时消除符号位影响((h ^ (h >>> 16)) & HASH_BITS
    • 确保哈希值为正数,因为负数哈希值有特殊含义(如 -1 表示 ForwardingNode

归纳:

  • table/nextTable:构建动态扩容的数据存储结构
  • sizeCtl:统一管理初始化与扩容流程
  • threshold:控制链表与红黑树的自适应转换
  • baseCount/CounterCell:高效统计元素数量
  • modCount:支持弱一致性迭代

1.8. 属性协作流程示例

1.8.1. 初始化流程

  1. 首次调用 put 时,检查 table 是否为 null
  2. 通过 CAS(sizeCtl, 0, -1) 抢占初始化权
  3. 创建长度为 16 的 table 数组
  4. 设置 sizeCtl 为扩容阈值(16×0.75 = 12)

1.8.2. 扩容触发与控制

  1. 元素数量超过 sizeCtl 时触发扩容
  2. 首个发起扩容的线程通过 CAS 将 sizeCtl 设置为特殊负值(如 - (resizeStamp(n) << RESIZE_STAMP_SHIFT + 2)
  3. 创建 nextTable(长度为原数组 2 倍)
  4. 多线程协作迁移节点,每个线程负责一段桶的迁移
  5. 迁移完成后,table 指向 nextTable,重置 sizeCtl 为新阈值

1.8.3. 数据结构转换

  1. 插入元素时,若链表长度达到 8 且数组长度≥64,将链表转换为红黑树(TreeBin
  2. 删除元素时,若红黑树节点数减少到 6,将红黑树转回链表。

2. 红黑树与 TreeBin 的关系

image.png

2.1 TreeBin 与 Node 节点的关系

1. TreeBin 的本质:红黑树的容器包装类

TreeBin 是 JDK8 ConcurrentHashMap 为实现红黑树结构而设计的容器类,它继承自 Node 以确保能存储在 Node 数组中,但实际的键值对存储在其内部的 TreeNode 节点中。这种设计既保持了数据结构的统一性(所有桶元素都是 Node 子类),又通过封装实现了链表与红黑树的平滑转换,是 JDK8 并发容器设计的重要优化点。

它的核心作用是将普通链表转换为红黑树时,对树结构进行封装。其类定义如下(JDK8 源码简化):

static final class TreeBin<K,V> extends Node<K,V> {
    TreeNode<K,V> root;        // 红黑树根节点
    TreeNode<K,V> first;       // 链表头节点(兼容遍历)
    int size;                  // 树节点数量
    // 其他并发控制相关属性...
}

2. TreeBin 与 Node 的继承关系

  • Node 是 ConcurrentHashMap 的基础节点抽象类
  • TreeBin 继承自 Node,但它不直接存储键值对,而是存储红黑树的根节点
  • 真正的红黑树节点由 TreeNode 类实现,它继承自 Node
static final class TreeNode<K,V> extends Node<K,V> {
    TreeNode<K,V> left;       // 左子节点
    TreeNode<K,V> right;      // 右子节点
    TreeNode<K,V> parent;     // 父节点
    boolean red;              // 红黑树颜色标记
    // 其他树节点属性...
}

3. 数据结构中的角色定位

image.png

4. 关键区别对比表

类型继承自存储内容核心作用
NormalNodeNodekey/value/next构成普通链表节点
TreeNodeNodekey/value/left/right构成红黑树节点,包含树结构指针
TreeBinNoderoot(TreeNode 根节点)封装红黑树,提供树操作接口

5. 红黑树转换时的结构变化

当链表长度超过 8 时,会发生以下转换:

  1. 原链表的头节点被替换为 TreeBin 实例
  2. TreeBin 的 root 指向新创建的红黑树根节点
  3. 原链表的所有 NormalNode 被转换为 TreeNode,构成红黑树结构
  4. TreeBin 的 first 指向原链表的头节点,保留链表遍历能力

6. 源码中的典型应用场景

// 当链表长度≥8时,转换为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
    treeifyBin(tab, i);

// treeifyBin方法核心逻辑(简化)
private final void treeifyBin(Node<K,V>[] tab, int index) {
    Node<K,V> b; int n, sc;
    if (tab != null) {
        // 创建TreeBin并替换原链表头节点
        tab[index] = new TreeBin<>(root);
    }
}

3. 并发控制核心机制

屏幕截图 2025-06-28 170959.png

3.1 锁粒度细化:从 Segment 到桶级锁

1. JDK8 的桶级锁架构

  • 锁粒度:直接对数组中的 ** 单个桶(头节点)** 加锁

    • 使用 synchronized 锁定桶的头节点(Node 或 TreeBin
    • 锁范围从整个 Segment 缩小到单个哈希桶

2. 锁优化带来的优势

  • 并发度提升:理论并发度等于数组长度(默认 16 → 32 → 64...)
  • 锁竞争减少:仅当多个线程同时访问同一桶时才会产生竞争
  • 锁效率更高synchronized 经过 JVM 优化(偏向锁、轻量级锁),性能接近 ReentrantLock

3. 源码关键实现

// putVal() 方法核心逻辑
synchronized (f) { // f 是桶的头节点
    if (tabAt(tab, i) == f) { // 再次校验头节点未变
        if (fh >= 0) { // 链表节点
            binCount = 1;
            for (Node<K,V> e = f;; ++binCount) {
                // 遍历链表...
            }
        }
        else if (f instanceof TreeBin) { // 红黑树节点
            // 红黑树插入...
        }
    }
}

3.2 CAS+volatile 无锁化操作

1. volatile 保证可见性

  • 关键字段

    • val 和 next 字段被声明为 volatile
    • table 和 nextTable 数组被声明为 volatile
  • 作用

    • 一个线程对节点的修改会立即刷新到主内存
    • 其他线程对节点的读取会直接从主内存获取最新值

2. CAS 实现原子操作

  • 典型应用场景

    • 初始化数组:使用 CAS(sizeCtl, 0, -1) 避免多线程重复初始化
    • 插入头节点:使用 CAS(tab, i, null, new Node) 创建头节点
    • 计数更新:使用 CAS(baseCount, expect, update) 或 CounterCell 分段计数

3. CAS 与锁的结合

  • 无锁优先:在无竞争场景下(如插入头节点),优先使用 CAS 避免加锁
  • 有锁兜底:在 CAS 失败或需要复杂操作时(如链表遍历),使用 synchronized 加锁

4. 源码关键实现

// 初始化数组时使用 CAS 避免冲突
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();
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { // CAS 抢占初始化权
            try {
                if ((tab = table) == null || tab.length == 0) {
                    tab = (Node<K,V>[])new Node<?,?>[sc]; // 创建数组
                    table = tab;
                    sc = n - (n >>> 2); // 计算下次扩容阈值
                }
            } finally {
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

3.3 并发控制的典型场景

1. 插入操作(put)

  1. 计算哈希值,定位桶位置

  2. CAS 检查桶是否为空:为空则直接 CAS 插入新节点

  3. 加锁操作:桶不为空则对头节点加锁

    • 链表:遍历并插入新节点
    • 红黑树:调用树的插入方法
  4. 检查链表长度:超过阈值(8)则转换为红黑树

2. 扩容操作(transfer)

  1. 触发条件:元素数量超过 sizeCtl
  2. CAS 抢占扩容权:第一个发起扩容的线程通过 CAS(sizeCtl, sc, (sc < 0) ? sc - 1 : resizeStamp(n) << RESIZE_STAMP_SHIFT + 2) 获取扩容资格
  3. 分段迁移:将原数组分为多个段,多线程各自负责一段的迁移
  4. 标记已迁移桶:迁移完成的桶用 ForwardingNode 替换原头节点,ForwardingNode 的 nextTable 指向新数组

3. 读取操作(get)

  1. 计算哈希值,定位桶位置

  2. 检查头节点类型

    • 普通节点 / 树节点:直接读取
    • ForwardingNode:通过 nextTable 到新数组读取
  3. 全程无锁:由于 val 和 next 是 volatile,保证读取的可见性

4、性能对比与优势

特性JDK7 ConcurrentHashMapJDK8 ConcurrentHashMap
锁粒度Segment(默认 16 个)单个桶(数组长度)
锁实现ReentrantLocksynchronized + CAS
并发度固定为 Segment 数量(默认 16)随数组扩容动态增加
哈希冲突处理仅链表链表 + 红黑树(长度 > 8 时转换)
读操作无锁,但需遍历 Segment无锁,直接定位桶
写操作锁定整个 Segment仅锁定单个桶

这种设计使 JDK8 ConcurrentHashMap 在高并发场景下的吞吐量显著提升,例如:

  • 读操作:几乎无锁竞争,性能接近无锁的 HashMap
  • 写操作:锁粒度细化后,并发写入的冲突概率大幅降低

3. 核心设计总结

  1. 数据结构三层形态

    • 数组:基础存储容器,初始 16 长度
    • 链表:哈希冲突少时使用,长度≤8
    • 红黑树:链表长度 > 8 时转换,查询 O (logN)
  2. 并发控制优化

    • 锁粒度细化:仅锁定操作桶的头节点
    • CAS+volatile:减少锁竞争,保证可见性
  3. 扩容机制

    • ForwardingNode 标记已迁移桶
    • 多线程协作迁移,读操作自动转发到新数组
    • 渐进式扩容,无全局停顿
  4. 性能关键点

    • 红黑树解决哈希冲突极端情况
    • 高位哈希扰动 (hash ^ (hash>>>16)) 提升分布均匀性
    • 懒初始化避免资源浪费