ConcurrentHashMap 源码逐行拆解:put/get 方法的并发安全执行流程

一、前言

在上一篇文章中,我们掌握了 JDK1.8 ConcurrentHashMap 的核心锁机制 ------CAS 无锁操作与桶级 synchronized 锁的协同逻辑。而这些机制最终都落地到 put 和 get 这两个核心方法中,它们的执行流程直接决定了 ConcurrentHashMap 的并发安全与存取效率。

本文将基于 JDK1.8 的 ConcurrentHashMap 源码,逐行拆解 put 和 get 方法的完整执行链路,厘清每一步的并发安全保障细节,带你从代码层面吃透高并发 Map 的存取逻辑。

二、ConcurrentHashMap 核心成员变量

在分析源码前,先明确几个核心成员变量的作用,这是理解方法逻辑的基础:

java 复制代码
// 哈希桶数组,存储键值对的核心容器
transient volatile Node<K,V>[] table;
// 扩容时的临时新数组
private transient volatile Node<K,V>[] nextTable;
// 控制数组初始化和扩容的核心标识,不同取值对应不同状态
// -1:数组初始化中;负数且≠-1:扩容中(值为resizeStamp<<16 + 线程数);正数:扩容阈值;0:默认初始容量
private transient volatile int sizeCtl;
// 链表转红黑树的阈值
static final int TREEIFY_THRESHOLD = 8;
// 红黑树转回链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;
// 触发树化的最小数组容量(避免小容量数组过度树化)
static final int MIN_TREEIFY_CAPACITY = 64;
// 节点hash值标识:MOVED=-1(扩容中转节点)、TREEBIN=-2(红黑树根节点容器)
static final int MOVED     = -1;
static final int TREEBIN   = -2;

三、put 方法:并发安全的元素插入流程

ConcurrentHashMap 的 put 方法对外暴露,实际核心逻辑在 putVal 方法中实现,调用链路为 put(K key, V value) → putVal(key, value, false) 。以下先展示完整源码,再逐段拆解。

1. put 与 putVal 方法完整源码

java 复制代码
// 对外暴露的put方法,不允许key/value为null
public V put(K key, V value) {
    return putVal(key, value, false);
}
/**
 * 内部核心插入方法
 * @param key 插入的键(不可为null)
 * @param value 插入的值(不可为null)
 * @param onlyIfAbsent 为true时,key存在则不覆盖旧值
 * @return 旧值(key存在时)或null(key不存在时)
 */
final V putVal(K key, V value, boolean onlyIfAbsent) {
    // 校验key和value,不允许为null,否则抛NPE
    if (key == null || value == null) throw new NullPointerException();
    // 计算key的哈希值(扰动处理,与HashMap一致)
    int hash = spread(key.hashCode());
    int binCount = 0; // 记录桶内节点数量,用于判断是否转红黑树
    for (Node<K,V>[] tab = table;;) { // 自旋保证操作成功
        Node<K,V> f; int n, i, fh;
        // 场景1:哈希桶数组未初始化,先初始化数组
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        // 场景2:目标桶为空,通过CAS无锁插入新节点
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
                break; // CAS成功,直接退出自旋
        }
        // 场景3:目标桶是扩容中转节点(MOVED),协助扩容
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        // 场景4:目标桶非空且非扩容状态,加锁处理
        else {
            V oldVal = null;
            // 对桶首节点加synchronized锁,保证桶内操作原子性
            synchronized (f) {
                // 二次校验桶首节点,防止加锁期间被修改
                if (tabAt(tab, i) == f) {
                    // 子场景4.1:桶内是普通链表结构
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            // 找到key相同的节点,处理旧值覆盖
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.value;
                                if (!onlyIfAbsent)
                                    e.value = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            // 遍历到链表尾部,尾插法插入新节点
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key, value, null);
                                break;
                            }
                        }
                    }
                    // 子场景4.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.value;
                            if (!onlyIfAbsent)
                                p.value = value;
                        }
                    }
                }
            }
            // 插入完成后,判断是否需要将链表转为红黑树
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i); // 树化(会先判断数组容量)
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    // 插入新节点后,更新元素数量并检查是否需要扩容
    addCount(1L, binCount);
    return null;
}

2. putVal 方法逐步骤拆解

步骤 1:null 值校验与哈希计算

ConcurrentHashMap 严格禁止 key 或 value 为 null ,若传入 null 会直接抛出 NullPointerException ,这是与 HashMap 的核心区别之一。随后通过 spread 方法完成哈希扰动,公式为 (h ^ (h >>> 16)) & 0x7fffffff ,既混合高低位特征,又保证哈希值为正数。

步骤 2:自旋处理多线程竞争

方法外层采用 for(;;) 自旋循环,保证即使 CAS 失败或需要协助扩容,也能通过重试完成最终插入,避免操作中断。

步骤 3:数组初始化(initTable)

若哈希桶数组 table 未初始化,调用 initTable 方法完成初始化。该方法通过 CAS + 自旋实现,保证多线程下仅能初始化一次(详见上一篇 CAS 应用场景)。

步骤 4:空桶的 CAS 无锁插入

通过 tabAt 方法(volatile 读)获取目标桶的首节点,确认桶为空后,调用 casTabAt 方法尝试原子插入新 Node。若 CAS 成功,直接退出自旋;若失败,说明有其他线程抢先插入,进入后续加锁流程。

步骤 5:协助扩容(helpTransfer)

若桶首节点的 hash 为 MOVED (-1),说明该桶是扩容中转节点(ForwardingNode),当前线程会调用 helpTransfer 方法协助扩容,将元素迁移至新数组,扩容完成后再继续插入操作。

步骤 6:非空桶的 synchronized 加锁处理

这是 putVal 的核心并发安全逻辑,对桶首节点加 synchronized 锁,保证同一桶内操作互斥,分两种子场景:

  • **子场景 6.1:普通链表处理:**遍历链表,若找到相同 key 则根据 onlyIfAbsent 参数决定是否覆盖旧值;若遍历至尾部,则通过 尾插法 插入新节点,同时记录链表长度 binCount 。

  • **子场景 6.2:红黑树处理:**若桶首节点是 TreeBin (红黑树容器),调用 putTreeVal 方法完成红黑树节点插入,该方法会维护红黑树的平衡特性。

步骤 7:链表转红黑树(treeifyBin)

若链表长度 binCount≥8 ,调用 treeifyBin 方法触发树化。该方法会先判断数组容量,若数组容量 TREEIFYCAPACITY(64) ,会先扩容数组而非直接树化;若容量达标,则将链表转为红黑树。

步骤 8:更新元素数量与扩容检查(addCount)

插入新节点后调用 addCount ,通过 CAS 原子更新元素总数。同时该方法会检查元素数量是否超过扩容阈值,若超过则触发 transfer 扩容方法,且支持多线程协作扩容。

四、get 方法:无锁化的并发查询流程

ConcurrentHashMap 的 get 方法全程无锁,通过 volatile 的可见性保证和结构化遍历实现高效查询,核心逻辑在 getNode 方法中,调用链路为 get(Object key) → getNode(hash(key), key) 。

1. get 与 getNode 方法完整源码

java 复制代码
// 对外暴露的get方法
public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    // 计算哈希值
    int h = spread(key.hashCode());
    // 校验数组有效且目标桶存在节点
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        // 场景1:桶首节点直接匹配
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.value;
        }
        // 场景2:桶内是扩容中转节点或红黑树,特殊处理
        else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.value : null;
        // 场景3:桶内是普通链表,遍历查询
        while ((e = e.next) != null) {
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.value;
        }
    }
    return null;
}
// Node类的find方法(普通Node默认实现,ForwardingNode和TreeBin会重写)
Node<K,V> find(int h, Object k) {
    Node<K,V> e = this;
    if (k != null) {
        do {
            K ek;
            if (e.hash == h &&
                ((ek = e.key) == k || (ek != null && k.equals(ek))))
                return e;
        } while ((e = e.next) != null);
    }
    return null;
}

2. get 方法逐步骤拆解

步骤 1:哈希计算与数组有效性校验

先通过 spread 方法计算 key 的哈希值,再校验哈希桶数组是否已初始化、目标桶是否存在节点,若任一条件不满足则直接返回 null 。

步骤 2:桶首节点直接匹配

优先对比桶首节点的哈希值和 key,若匹配则直接返回该节点的 value,这是查询的最优路径(无哈希冲突时)。

步骤 3:特殊节点的查询处理

若桶首节点的 hash<0 ,说明是特殊节点( MOVED 或 TREEBIN ),调用节点的 find 方法处理:

  • ForwardingNode(MOVED): find 方法会到新数组中查询目标 key;

  • TreeBin(TREEBIN): find 方法会通过红黑树的二叉查找逻辑定位节点,时间复杂度 O (log n)。

步骤 4:普通链表的遍历查询

若桶内是普通链表,循环遍历链表节点,逐一对比哈希值和 key,找到匹配节点则返回 value,遍历结束无匹配则返回 null 。

3. get 方法的并发安全保障

get 方法全程无锁,其并发安全依赖以下两点:

  1. **Node 属性的 volatile 修饰:**Node 类的 value 和 next 属性均被 volatile 修饰,保证多线程下的内存可见性,即一个线程修改节点值或链表指针后,其他线程能立即读取到最新值;

  2. **无修改操作:**get 仅为读操作,不涉及节点结构修改,无需加锁;若查询时遇到扩容,ForwardingNode 会引导至新数组查询,保证数据一致性。

五、put/get 方法的并发安全关键细节

1.加锁粒度的精准控制

put 方法仅对桶首节点加 synchronized 锁,锁粒度为 "桶级别",不同桶的操作可完全并发,同一桶的操作才会互斥,最大化降低锁竞争。

2.二次校验的防并发修改

在 synchronized 加锁后,会通过 tabAt(tab, i) == f 二次校验桶首节点,防止加锁期间其他线程修改了桶的结构(如扩容迁移、链表转红黑树),保证锁内操作基于最新的桶状态。

3.扩容时的读写协作

扩容过程中,原数组的桶会被替换为 ForwardingNode:

  • **写操作(put):**遇到 ForwardingNode 会先协助扩容,再执行插入;

  • **读操作(get):**遇到 ForwardingNode 会直接到新数组查询,不阻塞且能获取最新数据。

4.元素数量的原子更新

addCount 方法通过 CAS 原子更新元素总数,同时采用分段统计的方式(CounterCell 数组)避免高并发下的计数竞争,保证 size 统计的准确性。

六、put/get 方法的时间复杂度分析

put 方法

  • 无哈希冲突(CAS 插入):O (1),仅需哈希计算和 CAS 操作;

  • 链表结构:O (n),n 为链表长度,加锁后遍历链表;

  • 红黑树结构:O (log n),红黑树的插入操作时间复杂度为对数级别。

get 方法

  • 无哈希冲突(桶首匹配):O (1),直接返回桶首节点值;

  • 链表结构:O (n),遍历链表查询;

  • 红黑树结构:O (log n),二叉查找定位节点。

JDK1.8 引入红黑树后,大幅优化了哈希冲突严重时的查询和插入性能,保证了高并发场景下的性能稳定性。

七、结语

本文通过逐行拆解源码,完整梳理了 ConcurrentHashMap 的 put 和 get 方法执行流程: put 方法通过 "CAS 无锁插入→synchronized 桶级锁→协作扩容" 实现并发安全插入, get 方法基于 volatile 可见性和无锁遍历实现高效查询。

下一篇文章,我们将聚焦 ConcurrentHashMap 的扩容机制,深度解析高并发场景下的安全扩容实现,以及多线程协作扩容的底层逻辑,敬请关注!

相关推荐
码出财富9 小时前
SpringBoot 内置的 20 个高效工具类
java·spring boot·spring cloud·java-ee
多米Domi0119 小时前
0x3f第33天复习 (16;45-18:00)
数据结构·python·算法·leetcode·链表
我是小疯子669 小时前
Python变量赋值陷阱:浅拷贝VS深拷贝
java·服务器·数据库
森叶9 小时前
Java 比 Python 高性能的原因:重点在高并发方面
java·开发语言·python
二哈喇子!9 小时前
Eclipse中导入外部jar包
java·eclipse·jar
微露清风9 小时前
系统性学习C++-第二十二讲-C++11
java·c++·学习
罗湖老棍子9 小时前
【例4-11】最短网络(agrinet)(信息学奥赛一本通- P1350)
算法·图论·kruskal·prim
方圆工作室9 小时前
【C语言图形学】用*号绘制完美圆的三种算法详解与实现【AI】
c语言·开发语言·算法
Lips61110 小时前
2026.1.16力扣刷题
数据结构·算法·leetcode
进阶小白猿10 小时前
Java技术八股学习Day20
java·开发语言·学习