ConcurrentHashMap 重要方法实现原理和源码解析(一)

一、Node 内部类

ConcurrentHashMap 内部有一个内部类 Node,实现了 Map.Entry 接口,作为 HashMap 内部的链表节点。

核心特性

  • hashCodekey.hashCode() ^ val.hashCode()
  • toStringkey + "=" + val

源码实现

java 复制代码
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    volatile V val;
    volatile Node<K,V> next;
    
    Node(int hash, K key, V val, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.val = val;
        this.next = next;
    }
    
    public final K getKey()       { return key; }
    public final V getValue()     { return val; }
    public final int hashCode()   { return key.hashCode() ^ val.hashCode(); }
    public final String toString(){ return key + "=" + val; }
    public final V setValue(V value) {
        throw new UnsupportedOperationException();
    }
}

二、带初始大小的构造函数

核心逻辑

  1. 参数校验:如果初始化数小于 0,抛出非法数字异常
  2. 容量上限 :如果初始大小大于最大限制长度的一半,就设置为 MAXIMUM_CAPACITY
  3. 容量优化 :分两步理解
    • 步骤一 :先计算「扩容后候选值」:initialCapacity + (initialCapacity >>> 1) + 1
      • initialCapacity >>> 1:右移 1 位等价于「整数除法 2」(例如 initialCapacity=1010>>>1=5
      • 整体计算结果:initialCapacity * 1.5 + 1(例如 initialCapacity=1010+5+1=16initialCapacity=2020+10+1=31
      • 目的:预留充足空间,减少扩容次数
    • 步骤二 :通过 tableSizeFor 调整为「2 的幂」
      • tableSizeFor 是 ConcurrentHashMap 的核心工具方法,作用是:返回大于等于输入值的最小的 2 的幂(例如输入 16,输出 16;输入 31,输出 32;输入 58,输出 64)

为什么必须是 2 的幂?

  1. 哈希分布均匀 :底层哈希表的元素定位依赖「哈希值 & (容量 - 1)」(位运算替代取模,效率更高),而只有 2 的幂的「容量 - 1」是全 1 二进制(例如容量 16,15=0b1111),能保证哈希值均匀分布,避免哈希冲突集中
  2. 并发扩容基础:2 的幂容量是 ConcurrentHashMap 并发扩容(分段扩容、transfer 机制)的基础
  3. 高效扩容 :HashMap 在自动扩容的时候,是按两倍进行扩容的,两倍扩容的好处是原来的索引在两倍扩容之后计算新的索引,新索引只会有两种情况:
    • 一种是索引不变
    • 一种是索引长度加原来 HashMap 的长度
    • 数据迁移的时候,分割成两部分(红黑树或者子链)头结点拼接就好
    • 判断索引长度是否变化也只需要执行一条位运算语句就行((hash & oldCap) == 0 → 不变,== 1 → 长度加 oldCap

源码实现

java 复制代码
public ConcurrentHashMap(int initialCapacity) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException();
    int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) 
        ? MAXIMUM_CAPACITY 
        : tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
    this.sizeCtl = cap;
}

三、size() 方法

核心方法:sumCount()

sumCount() 是真正实现 HashMap 长度计算的关键方法。

源码实现

java 复制代码
public int size() {
    long n = sumCount();
    return ((n < 0L) ? 0 : (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int)n);
}

final long sumCount() {
    // 1. 拿到分段计数器数组的引用(as 是局部变量,避免遍历中数组被修改的影响)
    CounterCell[] as = counterCells;
    // 2. 临时变量,用于遍历中存储单个 CounterCell
    CounterCell a;
    // 3. 初始化总和:先累加基础计数器 baseCount 的值
    long sum = baseCount;
    // 4. 若分段计数器数组不为 null(说明存在高并发竞争,有分散的计数)
    if (as != null) {
        // 5. 遍历所有分段计数器
        for (int i = 0; i < as.length; ++i) {
            // 6. 若当前分段计数器不为 null,累加其 value 值
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    // 7. 返回总和(baseCount + 所有 CounterCell.value 的和)
    return sum;
}

四、get(key) 方法

实现说明

  1. 计算哈希值:获取传入参数 key 的 hashCode 值(进行二次 hash 减少碰撞)
  2. 前置校验:校验 HashMap 是否进行初始化,table 中是否有 hash 值相同的节点(若是用链表或者红黑树解决冲突,整个结构中的节点 hash 值都是一样的,所以返回头节点)
  3. 匹配逻辑
    • 情况一:当前节点(头节点),通过引用地址判断和 equals 内容判断符合,返回当前节点的 val
    • 情况二hash < 0,说明当前结构是特殊数据结构:红黑树,调用对应的 e.find() 方法查找是否有符合的节点(同样是引用地址一致或者内容一致)
    • 情况三 :普通的拉链法解决的 hash 冲突,while 循环调用 next() 进行判断
    • 情况四:没有匹配的返回 null

源码实现

java 复制代码
public V get(Object key) {
    // 局部变量:tab=哈希表数组,e=当前遍历的节点,p=特殊节点的查找结果;n=数组长度,eh=当前节点的hash,ek=当前节点的key
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    // 1. 计算key的最终哈希值(二次哈希,减少碰撞)
    int h = spread(key.hashCode());
    // 2. 校验哈希表是否初始化、数组长度有效,且目标索引存在节点(核心前置判断)
    if ((tab = table) != null && (n = tab.length) > 0 && (e = tabAt(tab, (n - 1) & h)) != null) {
        // 3. 情况1:当前节点的hash与计算的hash完全匹配(直接命中候选节点)
        if ((eh = e.hash) == h) {
            // 对比key:先==(地址/基本类型)再equals(内容),兼顾性能和正确性
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val; // 命中,返回value
        }
        // 4. 情况2:当前节点是特殊节点(eh<0,hash为负数)
        else if (eh < 0)
            // 调用特殊节点的find方法查找(适配扩容、红黑树等场景)
            return (p = e.find(h, key)) != null ? p.val : null;
        // 5. 情况3:普通链表节点,循环遍历查找
        while ((e = e.next) != null) {
            // 对比hash(快速排除)和key(精确匹配)
            if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    // 6. 未找到(table未初始化/索引无节点/遍历无匹配key)
    return null;
}

五、containsKey() 方法

实现说明

containsKey() 的实现很简单,就是内部调用 get(key),要是返回结果不为 null 说明存在,返回 null 说明不存在,返回 false。

六、containsValue(Object value) 方法

背景铺垫

  1. value 的特性 :与 key 不同,value 没有哈希值,无法通过哈希计算直接定位,只能通过「全表扫描」遍历所有节点判断,因此时间复杂度为 O(n)(n 为总键值对数量),效率远低于 get(key)(O(1) 或 O(logn))
  2. 并发安全基础:延续 ConcurrentHashMap 的核心设计 ------ table 数组是 volatile、Node.val 是 volatile,确保遍历过程中能读取到最新的节点数据;特殊节点(如扩容标记 ForwardingNode、红黑树容器 TreeBin)需特殊处理,避免遍历遗漏或读取脏数据
  3. 遍历器 Traverser:ConcurrentHashMap 的内部专用遍历器,负责「安全遍历哈希表的所有节点」,自动适配链表、红黑树、扩容等场景,无需手动处理特殊节点逻辑

实现说明

  1. 参数校验value == null 是非法参数,ConcurrentHashMap 不允许 value 为 null,否则返回空指针异常
  2. 获取 Traverser:全局遍历器,会从 table 的 0 序号位开始,一直遍历到最后
  3. 遍历匹配 :使用 Traverser.advance() 获取下一个节点,通过引用地址相同和 equals 内容相等进行判断

源码实现

java 复制代码
public boolean containsValue(Object value) {
    // 1. 非法参数校验:ConcurrentHashMap 不允许 value 为 null,直接抛空指针异常
    if (value == null)
        throw new NullPointerException();
    // 2. 局部变量:t = 哈希表数组
    Node<K,V>[] t;
    // 3. 校验哈希表是否已初始化(未初始化则直接返回 false,无任何键值对)
    if ((t = table) != null) {
        // 4. 创建全表遍历器 Traverser:遍历范围是整个哈希表(从索引 0 到 t.length-1)
        Traverser<K,V> it = new Traverser<K,V>(t, t.length, 0, t.length);
        // 5. 循环遍历所有节点:it.advance() 获取下一个有效节点,直到返回 null(遍历结束)
        for (Node<K,V> p; (p = it.advance()) != null; ) {
            // 6. 局部变量:v = 当前节点的 value
            V v;
            // 7. value 匹配逻辑:先 ==(地址/基本类型)再 equals(内容),兼顾性能和正确性
            if ((v = p.val) == value || (v != null && value.equals(v)))
                return true; // 匹配成功,立即返回 true
        }
    }
    // 8. 未找到匹配的 value(table 未初始化/遍历全表无匹配)
    return false;
}

七、put() 方法

说明

onlyIfAbsent 作用:

  • true:只有当 key 不存在的时候才能够放入
  • false:当 key 存在的时候会替换掉旧值

源码实现

java 复制代码
public V put(K key, V value) {
    // key、value、onlyIfAbsent
    return putVal(key, value, false);
}

八、putVal() 方法

putVal(K key, V value, boolean onlyIfAbsent) 是 ConcurrentHashMap 中并发安全的键值对插入/更新核心方法(put、putIfAbsent 等公开方法均依赖此方法),核心目标是:

  1. 在高并发场景下,安全地向哈希表插入键值对,避免数据竞争和脏读/写
  2. 支持两种语义:onlyIfAbsent=true 时「不存在则插入」(不覆盖已有值),onlyIfAbsent=false 时「存在则更新、不存在则插入」
  3. 自动处理哈希表初始化、并发扩容、链表红黑树转换等底层细节,兼顾并发安全性和操作效率

其设计核心是「细粒度锁(桶级锁)+ 无锁 CAS + 并发协作」:通过最小粒度的锁(仅锁定单个桶的头节点)减少锁竞争,空桶插入用 CAS 避免加锁,扩容时支持多线程协作,最大化并发性能。

背景铺垫

核心约束

  • key 和 value 均不允许为 null(与 HashMap 区别),避免并发场景下的歧义;ConcurrentHashMap 是线程安全的,在 JDK 1.8 之后使用了 synchronized 锁,持有锁的对象是 Map.Entry,要是允许 key 和 value 为 null,会出现并发歧义

锁机制:Java 8+ 取消分段锁(锁的粒度大,并发效率低),改用「桶级 synchronized 锁」(锁定每个桶的头节点),锁粒度更小,并发冲突概率更低

节点类型

  • 普通链表节点(Node):hash >= 0,存储键值对和 next 指针
  • 特殊节点:hash < 0MOVED=-1 扩容标记、TreeBin=-2 红黑树容器)

并发扩容 :支持多线程协同扩容(helpTransfer),避免单线程扩容瓶颈(导致扩容的线程,扩容过程中执行 put 方法的线程都加进来)

自适应结构:链表长度超过阈值(默认 8)且表容量 ≥ 64 时,自动转为红黑树(TreeBin),优化查询效率,要是 table 的大小没有达到 64,不会升级为红黑树,而是执行扩容。

实现说明

  1. 参数校验:key 或者 value 为 null,抛出 NullPointer 异常
  2. 计算哈希值 :获取 key 的二次哈希值,设置 binCount,作用是记录链表或者红黑树节点数,用于判断是否要考虑将当前的链表转为红黑树
  3. 死循环重试:进入一个死循环的 for,因为并发场景下可能会出现没有初始化,或者需要扩容后重新定位,链表升级为红黑树之后的重新定位,所以设计死循环,没有出口,直到数据存储成功了才会退出
  4. 四个场景处理
    • 场景一 :table 为 null 或者 table 的长度为 0,通过 initTable() 方法进行初始化(线程安全的初始化方法(CAS 控制,避免重复初始化))
    • 场景二 :通过 tabAt 方法获取桶列表(索引所在节点),要是为 null,说明该索引目前没有使用,不存在 hash 冲突和旧值,通过 casTabAt 方法插入新节点(目标桶为空(通过 CAS 原子插入节点,无锁操作(乐观锁操作)),CAS 插入新节点:对比 tab[i] 是否为 null,是则插入新 Node,成功则跳出循环,空桶插入无锁,性能最优)
    • 场景三 :目标桶的头节点是扩容标记(MOVED=-1)协助扩容,else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); // 当前线程参与扩容,扩容后重新获取 table 重试
    • 场景四 :目标桶有节点(非空、非扩容)加锁插入/更新(桶级锁)
      • 通过 synchronized (f),锁定当前桶的头节点 f,锁粒度为桶,不影响其他桶的操作,并发效率高
      • 通过 tabAt(tab, i) == f,进行二次验证确保加锁前,当前桶的头节点未被其他线程修改(如扩容迁移、转红黑树)
      • 子场景 4.1 :头节点是链表节点(fh>=0
        • binCount = 1; // 初始化链表节点计数(从 1 开始,包含头节点)
        • for (Node<K,V> e = f;; ++binCount) // 遍历链表,查找 key 是否存在
        • 找到相同 key:判断是否需要更新 value
        • 保存前置节点,遍历到链表尾部:插入新节点到尾部
      • 子场景 4.2 :头节点是红黑树容器(TreeBin,fh=-2
        • binCount = 2; // 红黑树节点计数设为 2(无需精确计数,仅用于触发转树判断)
        • 调用红黑树的插入方法,返回已存在的节点(若 key 已存在)
  5. 后续处理 :循环的最后,判断是否执行扩容或者升级为红黑树操作:
    • 加锁操作后:处理链表转红黑树和返回值
    • 若链表长度 ≥ 阈值(默认 8),触发链表转红黑树(或扩容)
    • 若 key 已存在,返回旧 value
    • 插入新节点成功,跳出循环
  6. 计数更新:插入成功后,更新哈希表的键值对计数(baseCount 或 CounterCell)

源码实现

java 复制代码
final V putVal(K key, V value, boolean onlyIfAbsent) {
    // 1. 非法参数校验:key/value 为 null 直接抛空指针(ConcurrentHashMap 禁止 null 键值)
    if (key == null || value == null) throw new NullPointerException();
    // 2. 二次哈希:减少哈希碰撞,保证 key 均匀分布(与 get 方法的 spread 逻辑一致)
    int hash = spread(key.hashCode());
    // 3. 桶内节点计数:用于判断是否需要将链表转为红黑树
    int binCount = 0;
    // 4. 死循环重试:并发场景下需重试(如初始化、扩容后需重新定位),直到插入/更新成功
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        // 场景 1:哈希表未初始化(tab==null)或容量为 0  初始化 table
        if (tab == null || (n = tab.length) == 0)
            tab = initTable(); // 线程安全的初始化方法(CAS 控制,避免重复初始化)
        // 场景 2:目标桶为空(通过 CAS 原子插入节点,无锁操作)
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // CAS 插入新节点:对比 tab[i] 是否为 null,是则插入新 Node,成功则跳出循环
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
                break; // 空桶插入无锁,性能最优
        }
        // 场景 3:目标桶的头节点是扩容标记(MOVED=-1) 协助扩容
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f); // 当前线程参与扩容,扩容后重新获取 table 重试
        // 场景 4:目标桶有节点(非空、非扩容) 加锁插入/更新(桶级锁)
        else {
            V oldVal = null;
            // 锁定当前桶的头节点 f:锁粒度为桶,不影响其他桶的操作,并发效率高
            synchronized (f) {
                // 二次校验:确保加锁前,当前桶的头节点未被其他线程修改(如扩容迁移、转红黑树)
                if (tabAt(tab, i) == f) {
                    // 子场景 4.1:头节点是链表节点(fh>=0)
                    if (fh >= 0) {
                        binCount = 1; // 初始化链表节点计数(从 1 开始,包含头节点)
                        // 遍历链表,查找 key 是否存在
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            // 找到相同 key:判断是否需要更新 value
                            if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) {
                                oldVal = e.val; // 记录旧 value
                                if (!onlyIfAbsent) // onlyIfAbsent=false 时覆盖旧值
                                    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;
                            }
                        }
                    }
                    // 子场景 4.2:头节点是红黑树容器(TreeBin,fh=-2)
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2; // 红黑树节点计数设为 2(随便设置的,只要不大于8就可以了,目的是不触发下面的判断是否要进行链表转树操作)
                        // 调用红黑树的插入方法,返回已存在的节点(若 key 已存在)
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent) // 覆盖旧值
                                p.val = value;
                        }
                    }
                }
            }
            // 加锁操作后:处理链表转红黑树和返回值
            if (binCount != 0) {
                // 若链表长度阈值(默认 8),触发链表转红黑树(或扩容)(已经是红黑树的默认设置为2,永远不会触发)
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                // 若 key 已存在,返回旧 value
                if (oldVal != null)
                    return oldVal;
                break; // 插入新节点成功,跳出循环
            }
        }
    }
    // 5. 插入成功后,更新哈希表的键值对计数(baseCount 或 CounterCell)
    addCount(1L, binCount);
    // 6. 插入新节点(key 不存在),返回 null
    return null;
}

计数更新(addCount)

插入新节点后,调用 addCount(1L, binCount) 更新总键值对数量; 计数逻辑:延续 sumCount 的设计,低并发时更新 baseCount(CAS),高并发时分散到 CounterCell[] 数组,避免计数竞争; binCount 作用:辅助判断是否需要扩容(若总计数 ≥ sizeCtl 阈值,触发扩容)。

后续:juejin.cn/spost/75738...

相关推荐
javaTodo3 分钟前
Claude Code 记忆机制详解:从 CLAUDE.md 到 Auto Memory,六层体系全拆解
后端
LSTM9724 分钟前
使用 C# 和 Spire.PDF 从 HTML 模板生成 PDF 的实用指南
后端
JaguarJack35 分钟前
为什么 PHP 闭包要加 static?
后端·php·服务端
BingoGo1 小时前
为什么 PHP 闭包要加 static?
后端
是糖糖啊1 小时前
OpenClaw 从零到一实战指南(飞书接入)
前端·人工智能·后端
百度Geek说1 小时前
基于Spark的配置化离线反作弊系统
后端
后端AI实验室2 小时前
用AI写代码,我差点把漏洞发上线:血泪总结的10个教训
java·ai
Java编程爱好者2 小时前
虚拟线程深度解析:轻量并发编程的未来趋势
后端
苏三说技术2 小时前
Spring AI 和 LangChain4j ,哪个更好?
后端