ConcurrentHashMap 核心原理与源码深度解析

        在之前我们学过了HashTable,我们知道它是线性安全的,看他的源代码我们能够发现它是采用synchronized同步锁锁住了整个put方法,get 方法不加锁,但是HashTable 内部几乎所有写方法比如:(put/get/size/clear)都加了 synchronized,这意味着,在同一时间,整个 HashTable 只能有一个线程操作,哪怕你只是读两个不同 key,也要排队。在高并发下直接变成串行执行,性能极差!

        因此在jdk1.8之后,我们大多选择采用ConcurrentHashMap ,相对于HashTable 锁太粗暴,ConcurrentHashMap 又安全又快,现代并发场景几乎全面碾压。ConcurrentHashMap采用 CAS + 桶级细粒度锁,读无锁,并发能力极强。

        接下来我们来以基于 JDK1.8 版本,从数据结构、putVal 核心插入流程、get 核心查询流程三个维度,拆解ConcurrentHashMap其底层实现与并发安全机制。

一、核心数据结构(JDK1.8 演进核心)

JDK1.8 彻底摒弃了 JDK1.7 的 Segment 分段锁,采用 数组 + 链表 + 红黑树 的三层结构,锁粒度从 Segment 级细化到哈希桶(Node 节点)级,并发吞吐量大幅提升。
1.ConcurrentHashMap的核心组成:

2.核心控制字段

sizeCtl:控制标识,-1 表示正在初始化,-N 表示有 N 个线程正在扩容,≥0 表示阈值或容量;

baseCount:元素个数基准值,配合 CounterCell 实现高效计数;

nextTable:扩容时的新数组,仅在扩容期间非空。

1.3 结构转换规则(同HashMap)

哈希冲突时,先以链表形式存储;

链表长度≥8 且数组长度≥64:链表转为红黑树(降低查询复杂度从 O (n) 到 O (log n));

链表长度 < 8:红黑树转回链表(扩容时触发);

数组长度 < 64:优先扩容数组,不树化(避免小容量下的树化开销)。

二、putVal () 方法核心源码解读

2.1 put 方法最终调用 putVal 内部方法,是 ConcurrentHashMap 并发插入的核心,全程遵循 CAS 无锁 + synchronized 细粒度锁 原则,核心流程共 7 步。

详细解读代码每一步骤:

public V put(K key, V value) {
    return putVal(key, value, false);
}

final V putVal(K key, V value, boolean onlyIfAbsent) {
    // 1. 空值校验:key/value 均不允许为 null(与 HashMap 一致)
    if (key == null || value == null) throw new NullPointerException();
    // 2. 哈希扰动:减少哈希冲突,提升分布均匀性
    int hash = spread(key.hashCode());
    int binCount = 0; // 记录桶内节点数,用于树化判断

    // 3. 自旋:保证插入成功(失败则重试)
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        // 4. 数组初始化:首次 put 时触发,CAS 保证线程安全
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        // 5. 空桶 CAS 插入:无锁操作,竞争小则直接插入
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
        // !!!去除上面计算出的下标位置上的所在的元素,判断是否为空
        //  如果不为空说明当前下标位置镁元素,接下来进行CAS 判断
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
                break; // 插入成功,跳出自旋
        }
        // 6. 扩容协助:当前桶正在扩容,参与数据迁移
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        // 7. 哈希冲突:加锁桶头节点,处理链表/红黑树插入
        else {
            V oldVal = null;
            synchronized (f) { // 仅锁当前桶头,锁粒度极细
                if (tabAt(tab, i) == f) { // 双重检查:避免并发修改导致失效
                    // 7.1 链表处理:遍历插入/覆盖
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            // key 已存在:覆盖旧值(onlyIfAbsent 控制)
                            if (e.hash == hash &&
                                ((ek = e.key) == key || (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            // 链表尾部插入新节点
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key, value, null);
                                break;
                            }
                        }
                    }
                    // 7.2 红黑树处理:树节点插入/覆盖
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            // 8. 树化判断:链表长度达标则转红黑树
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    // 9. 计数更新:累加元素个数,检查扩容
    addCount(1L, binCount);
    return null;
}

具体 CAS方法 如何进行判断和插入,底层有点复杂,本章暂且先不探讨。

2.2 具体关键步骤拆解

步骤 1:空值与哈希预处理
  • 禁止 key/value 为 null(与 Hashtable 一致,与 HashMap 不同),直接抛出 NPE;
  • 调用 spread () 扰动 hash 值(高位参与运算),减少哈希冲突,保证分布均匀。
步骤 2:数组初始化(initTable)
  • 首次 put 时 table 为空,通过 CAS 初始化数组,避免多线程重复初始化;
  • 核心逻辑:通过 sizeCtl 控制初始化状态,CAS 保证只有一个线程完成初始化。
步骤 3:空桶 CAS 插入

         // !!!去除上面计算出的下标位置上的所在的元素,判断是否为空
        //  如果不为空说明当前下标位置镁元素,接下来进行CAS 判断

  • 计算桶索引 i = (n - 1) & hash,通过 tabAt () 获取桶头节点;
  • 桶为空时,调用 casTabAt () 无锁插入,竞争小则直接成功,无需加锁。
步骤 4:扩容协助(helpTransfer)
  • 桶头节点 hash 为 MOVED(-1),表示当前桶正在迁移数据;
  • 当前线程暂停插入,调用 helpTransfer () 参与数据迁移,提升扩容效率。
步骤 5:哈希冲突加锁处理
  • 桶非空且未扩容,对桶头节点 f 加 synchronized 锁(仅锁当前桶,不影响其他桶);
  • 双重检查 tabAt(tab, i) == f:避免并发修改导致桶头失效,保证线程安全。
步骤 6:链表 / 红黑树插入
  • 链表处理:遍历链表,key 存在则覆盖(onlyIfAbsent 控制),不存在则尾部插入;
  • 红黑树处理:调用 TreeBin.putTreeVal (),逻辑与红黑树插入一致,保证 O (log n) 效率。
步骤 7:树化与计数
  • 链表长度≥8 且数组长度≥64,调用 treeifyBin () 转红黑树;数组长度 < 64 时优先扩容;
  • 调用 addCount () 累加元素个数,同时检查是否触发扩容。

三、get () 方法核心源码解读

3.1get 方法是 ConcurrentHashMap 高并发读取的核心,全程无锁,通过 volatile 保证可见性,查询效率与 HashMap 接近。

public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    // 1. 哈希扰动:与 put 保持一致
    int h = spread(key.hashCode());
    // 2. 快速校验:数组非空、长度有效、桶非空
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        // 3. 桶头节点匹配:直接返回
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        // 4. 特殊节点处理:红黑树/扩容中转节点
        else if (eh < 0) {
            // 红黑树:调用 find 方法
            // 扩容节点:从新表 nextTable 查询
            return (p = e.find(h, key)) != null ? p.val : null;
        }
        // 5. 链表遍历:逐个匹配 key
        while ((e = e.next) != null) {
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    // 未找到:返回 null
    return null;
}

3.2 关键步骤拆解

步骤 1:哈希与快速校验
  • 计算哈希值 h,与 put 流程一致,保证定位准确;
  • 快速校验 table 非空、长度 n>0、目标桶 (n-1)&h 非空,避免无效查询。
步骤 2:桶头节点匹配
  • 桶头节点的 hash、key 与目标完全一致,直接返回 val(volatile 保证最新值)。
步骤 3:特殊节点查询
  • eh<0 表示当前节点是红黑树(TreeBin)或扩容中转节点(ForwardingNode);
  • 红黑树:调用 e.find (),O (log n) 效率查询;
  • 扩容节点:从新表 nextTable 查询,保证扩容期间查询结果准确。
步骤 4:链表遍历
  • 遍历链表,逐个匹配 hash 与 key,找到则返回 val,未找到则返回 null。

3.3无锁安全核心

  • volatile 保证可见性:table、Node.val 均为 volatile 修饰,get 能立即获取其他线程的修改结果;
  • 无锁设计:全程不加锁,避免锁竞争带来的性能开销,读性能远优于 Hashtable;
  • 一致性保证:遍历过程中,即使有并发修改(如插入、删除),也能基于当前桶的状态查询,不会抛出 ConcurrentModificationExceptionOracle。

四、核心总结与对比

4.1核心优势

4.2 适用场景

  1. 高并发读写场景:如缓存、配置中心、多线程数据聚合;
  2. 读多写少场景:无锁读保证高性能,写操作通过细粒度锁保证安全;
  3. 对性能要求高的哈希集合替代方案:优于 Hashtable,避免 HashMap 的线程安全风险。

4.3 避坑要点

  1. 禁止 key/value 为 null,否则抛出 NPE;
  2. 树化阈值为 8,数组长度≥64 才会树化,小容量场景优先扩容;
  3. 聚合方法(size、isEmpty、containsValue)结果可能不准确,仅适用于监控,不适合业务逻辑控制Oracle;
  4. 迭代器弱一致:遍历过程中允许修改,不会抛 ConcurrentModificationException,但不保证遍历到最新修改的所有元素Oracle。

五、总结

        ConcurrentHashMap 是 JDK1.8 并发集合的核心实现,通过 数组 + 链表 + 红黑树 的结构升级、CAS 无锁 + synchronized 细粒度锁 的并发控制、多线程协助扩容 的性能优化,实现了线程安全与高性能的平衡。putVal 方法的核心是 “空桶 CAS、冲突加锁、扩容协助”,get 方法的核心是 “无锁查询、volatile 可见性”,这两大流程共同支撑了 ConcurrentHashMap 在高并发场景下的稳定运行。

6.扩展

什么是CAS?

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐