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

前一篇:[ConcurrentHashMap 重要方法实现原理和源码解析(一)(juejin.cn/post/757389...)

九、treeifyBin() 方法

核心作用

链表转红黑树或者扩容方法。

核心常量

  • MIN_TREEIFY_CAPACITY:触发红黑树转换的最小哈希表容量,默认值 64(容量小于此值时,优先扩容而非转树)
  • TreeNode<K,V>:红黑树的节点类型,继承 Node,额外包含红黑树所需的 prev(前驱)、next(后继)、parent(父节点)、left(左子树)、right(右子树)、red(颜色标记)字段
  • TreeBin<K,V>:红黑树的「容器节点」,封装 TreeNode 组成的红黑树,负责处理并发场景下的查询、插入、删除等操作(避免直接暴露 TreeNode 导致的并发安全问题)

源码实现

java 复制代码
private final void treeifyBin(Node<K,V>[] tab, int index) {
    // 局部变量:b=目标桶的头节点;n=哈希表容量;sc=扩容控制参数(辅助tryPresize)
    Node<K,V> b; int n, sc;
    // 1. 校验哈希表非空(未初始化则无需处理)
    if (tab != null) {
        // 场景 1:哈希表容量 < 最小转树容量(64)→ 优先扩容,不转树
        if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
            tryPresize(n << 1); // 扩容为原容量的 2 倍(n<<1 等价于 n*2)
        // 场景 2:哈希表容量≥64,且目标桶的头节点是普通链表节点(b.hash≥0)
        else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
            // 2. 锁定目标桶的头节点:桶级锁,保证转换过程中桶内数据不被并发修改
            synchronized (b) {
                // 3. 二次校验:确保加锁前,头节点未被其他线程修改(如扩容迁移、已转树)
                if (tabAt(tab, index) == b) {
                    // 局部变量:hd=红黑树根节点;tl=红黑树尾节点(构建TreeNode双向链表用)
                    TreeNode<K,V> hd = null, tl = null;
                    // 4. 遍历链表,将所有普通 Node 转为 TreeNode
                    for (Node<K,V> e = b; e != null; e = e.next) {
                        // 创建 TreeNode:复制原 Node 的 hash、key、value,prev/next 暂为 null
                        TreeNode<K,V> p = new TreeNode<K,V>(e.hash, e.key, e.val, null, null);
                        // 构建 TreeNode 双向链表(prev 前驱,next 后继)
                        if ((p.prev = tl) == null)
                            hd = p; // 第一个节点作为链表头(后续将成为红黑树根)
                        else
                            tl.next = p; // 前一个节点的 next 指向当前节点
                        tl = p; // 尾节点指针后移
                    }
                    // 5. 用 TreeBin 包装 TreeNode 双向链表,替换原桶的头节点
                    setTabAt(tab, index, new TreeBin<K,V>(hd));
                }
            }
        }
    }
}

十、putAll() 方法

实现说明

先扩容,再循环执行 put() 方法。

源码实现

java 复制代码
public void putAll(Map<? extends K, ? extends V> m) {
    tryPresize(m.size());
    for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
        putVal(e.getKey(), e.getValue(), false);
}

十一、remove() 方法

公开 remove 方法

直接调用 replaceNode,传入 value=nullcv=null(无条件删除)。

源码实现

java 复制代码
public V remove(Object key) {
    return replaceNode(key, null, null);
}

十二、replaceNode() 方法

核心作用

底层核心实现:支持删除、替换、条件替换。

实现说明

  1. 二次哈希 :和 putValget 方法一致,减少哈希碰撞
  2. 死循环重试:应对并发场景(初始化、扩容、CAS 失败),直到操作完成
  3. 三个场景处理
    • 场景1:哈希表未初始化 或 目标桶为空 → 无对应 key,直接跳出循环返回 null
    • 场景2 :目标桶头节点是扩容标记(MOVED=-1)→ 协助扩容后重试
    • 场景3 :目标桶有节点(非空、非扩容)→ 桶级加锁执行操作
      • 子场景3.1 :头节点是链表节点(fh≥0
        • 遍历链表查找目标节点
        • 匹配 key:hash 相等 + key 地址/内容相等
        • 匹配条件 cv:cv 为 null(无条件)或 cv 与当前 value 相等
        • 执行操作:value 非空则替换,为空则删除
      • 子场景3.2 :头节点是红黑树容器(TreeBin)
        • 查找红黑树中的目标节点(findTreeNode 方法遍历红黑树)
        • 匹配 cv 条件
        • 执行操作:替换 value 或 删除节点
        • 红黑树节点数 ≤ 6,回退为链表(untreeify 方法)
  4. 后续处理:操作完成后,若仅删除操作需要更新计数(减少 1)

源码实现

java 复制代码
final V replaceNode(Object key, V value, Object cv) {
    // 1. 二次哈希:和putVal、get方法一致,减少哈希碰撞
    int hash = spread(key.hashCode());
    // 2. 死循环重试:应对并发场景(初始化、扩容、CAS失败),直到操作完成
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        // 场景1:哈希表未初始化 或 目标桶为空 → 无对应key,直接跳出循环返回null
        if (tab == null || (n = tab.length) == 0 ||
            (f = tabAt(tab, i = (n - 1) & hash)) == null)
            break;
        // 场景2:目标桶头节点是扩容标记(MOVED=-1)→ 协助扩容后重试
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        // 场景3:目标桶有节点(非空、非扩容)→ 桶级加锁执行操作
        else {
            V oldVal = null; // 存储被删除/替换的旧值
            boolean validated = false; // 标记是否找到并处理目标节点
            // 锁定桶的头节点:仅影响当前桶,不阻塞其他桶操作
            synchronized (f) {
                // 二次校验:确保加锁前头节点未被修改(如扩容迁移、转树)
                if (tabAt(tab, i) == f) {
                    // 子场景3.1:头节点是链表节点(fh≥0)
                    if (fh >= 0) {
                        validated = true;
                        Node<K,V> pred = null; // 前驱节点(用于删除时调整指针)
                        // 遍历链表查找目标节点
                        for (Node<K,V> e = f;;) {
                            K ek;
                            // 匹配key:hash相等 + key地址/内容相等
                            if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) {
                                V ev = e.val;
                                // 匹配条件cv:cv为null(无条件)或 cv与当前value相等
                                if (cv == null || cv == ev || (ev != null && cv.equals(ev))) {
                                    oldVal = ev; // 记录旧值
                                    // 执行操作:value非空则替换,为空则删除
                                    if (value != null)
                                        e.val = value; // 替换value(对应replace方法)
                                    else if (pred != null)
                                        pred.next = e.next; // 非头节点:前驱节点指向当前节点的下一个
                                    else
                                        setTabAt(tab, i, e.next); // 头节点:直接替换桶的头节点为下一个
                                }
                                break; // 操作完成,跳出遍历
                            }
                            pred = e; // 前驱节点后移
                            if ((e = e.next) == null)
                                break; // 遍历结束未找到,跳出
                        }
                    }
                    // 子场景3.2:头节点是红黑树容器(TreeBin)
                    else if (f instanceof TreeBin) {
                        validated = true;
                        TreeBin<K,V> t = (TreeBin<K,V>)f;
                        TreeNode<K,V> r, p;
                        // 查找红黑树中的目标节点(findTreeNode方法遍历红黑树)
                        if ((r = t.root) != null && (p = r.findTreeNode(hash, key, null)) != null) {
                            V pv = p.val;
                            // 匹配cv条件
                            if (cv == null || cv == pv || (pv != null && cv.equals(pv))) {
                                oldVal = pv;
                                // 执行操作:替换value 或 删除节点
                                if (value != null)
                                    p.val = value; // 替换value
                                else if (t.removeTreeNode(p)) // 删除红黑树节点,返回是否需要转链表
                                    // 红黑树节点数≤6,回退为链表(untreeify方法)
                                    setTabAt(tab, i, untreeify(t.first));
                            }
                        }
                    }
                }
            }
            // 操作完成后的后续处理
            if (validated) {
                if (oldVal != null) {
                    if (value == null) // 仅删除操作需要更新计数(减少1)
                        addCount(-1L, -1);
                    return oldVal; // 返回被删除/替换的旧值
                }
                break; // 未找到目标节点,跳出循环
            }
        }
    }
    return null; // 未找到key,返回null
}

十三、values() 和 entrySet() 方法

ConcurrentHashMap 的 values()entrySet() 方法,均返回基于底层数据的「视图(View)」而非拷贝,核心设计逻辑一致(懒加载 + 缓存复用、弱一致性),但视图类型、支持的操作及用途不同:

  • values() :返回 Collection<V> 类型视图,仅关注「值」,支持通过视图删除映射,不支持添加
  • entrySet() :返回 Set<Map.Entry<K,V>> 类型视图,关联「键值对」,支持删除映射和通过 Entry.setValue() 修改值,不支持添加

两者均避免一次性拷贝所有数据(节省内存 + 实时反映底层变化),适配高并发场景的遍历/修改需求。

源码实现

java 复制代码
public Set<Map.Entry<K,V>> entrySet() {
    EntrySetView<K,V> es;
    // values():缓存字段 values(volatile ValuesView<K,V>)
    return (es = entrySet) != null ? es : (entrySet = new EntrySetView<K,V>(this));
}

public Collection<V> values() {
    ValuesView<K,V> vs;
    // entrySet():缓存字段 entrySet(volatile EntrySetView<K,V>)
    return (vs = values) != null ? vs : (values = new ValuesView<K,V>(this));
}

十四、toString() 方法

实现说明

  1. 创建 Traverser 全表遍历器
  2. 创建 StringBuilder 进行字符串的拼接

源码逐行解析

java 复制代码
public String toString() {
    // 局部变量:t=哈希表数组;f=table长度(未初始化则为0)
    Node<K,V>[] t;
    int f = (t = table) == null ? 0 : t.length;
    // 1. 创建全表遍历器:遍历范围0~f-1(覆盖所有桶)
    Traverser<K,V> it = new Traverser<K,V>(t, f, 0, f);
    // 2. 初始化字符串构建器(高效拼接字符串,避免String频繁创建)
    StringBuilder sb = new StringBuilder();
    sb.append('{'); // 起始符
    Node<K,V> p;
    // 3. 读取第一个有效节点(it.advance()返回下一个节点,null表示遍历结束)
    if ((p = it.advance()) != null) {
        // 4. 循环遍历所有节点,拼接键值对
        for (;;) {
            K k = p.key;
            V v = p.val;
            // 拼接key:若key是当前map实例,替换为"(this Map)"(防循环引用)
            sb.append(k == this ? "(this Map)" : k);
            sb.append('=');
            // 拼接value:若value是当前map实例,替换为"(this Map)"(防循环引用)
            sb.append(v == this ? "(this Map)" : v);
            // 读取下一个节点,无节点则跳出循环
            if ((p = it.advance()) == null)
                break;
            sb.append(',').append(' '); // 分隔符
        }
    }
    // 拼接结束符,返回最终字符串
    return sb.append('}').toString();
}
相关推荐
skeletron20112 小时前
【PowerJob语雀转载】 官方处理器
后端
有梦想的攻城狮2 小时前
初识Rust语言
java·开发语言·rust
小虾米 ~2 小时前
RocketMQ DefaultMQPushConsumer vs DefaultLitePullConsumer
java·rocketmq·java-rocketmq
q***21602 小时前
【监控】spring actuator源码速读
java·spring boot·spring
Kuo-Teng2 小时前
LeetCode 142: Linked List Cycle II
java·算法·leetcode·链表·职场和发展
Moe4883 小时前
ConcurrentHashMap 重要方法实现原理和源码解析(一)
java·后端
hweiyu003 小时前
GO的优缺点
开发语言·后端·golang
大橙子打游戏3 小时前
在Xcode里自由使用第三方大模型?这个本地代理工具帮你实现!
后端
h***34633 小时前
Nginx 缓存清理
android·前端·后端