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...

相关推荐
hweiyu001 小时前
GO的优缺点
开发语言·后端·golang
大橙子打游戏1 小时前
在Xcode里自由使用第三方大模型?这个本地代理工具帮你实现!
后端
h***34631 小时前
Nginx 缓存清理
android·前端·后端
程序员爱钓鱼2 小时前
Python编程实战:用好 pdb 和 logging,程序再也不黑箱运行了
后端·python·trae
程序员爱钓鱼2 小时前
Python编程实战:从 timeit 到 cProfile,一次搞懂代码为什么慢
后端·python·trae
拾忆,想起2 小时前
Dubbo核心架构全解析:构建微服务通信的高速公路
java·微服务·云原生·架构·dubbo·哈希算法
JaguarJack2 小时前
进阶学习 PHP 中的二进制和位运算
后端·php
Moment2 小时前
专为 LLM 设计的数据格式 TOON,可节省 60% Token
前端·javascript·后端
楠枬2 小时前
Spring Cloud 概述
java·spring cloud·微服务