并发编程原理与实战(三十八)高并发利器ConcurrentHashMap 数据结构与核心API深度剖析

上一篇介绍了ConcurrentHashMap的核心特性,本文来分析ConcurrentHashMap的核心数据结构和API方法。

核心数据结构

ConcurrentHashMap的核心数据结构是:数组(Node[] table)+链表+红黑树的形式,而这核心数据结构主要由一个基础父类节点和四个子类节点构成。

基础节点Node

java 复制代码
public class ConcurrentHashMap<K, V> {
    // 哈希表数组,存储Node节点
    transient volatile Node<K, V>[] table;

    // 内部类:Node节点,存储键值对及下一个节点的引用
    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的构造函数等...
	}
}

Node节点是哈希表结构的基础存储单元,作为其他四种节点类型的父类。Node是普通的Entry结点,以链表形式存储实际数据。其定义包含以下关键字段:

final int hash:键的哈希值

final K key:不可变的键

volatile V val:使用volatile修饰的值,保证可见性

volatile Node<K,V> next:链表指针,同样使用volatile修饰

当发生哈希冲突时,新节点会以链表形式连接在桶位的头节点之后。

红黑树容器TreeBin和节点TreeNode

java 复制代码
// 内部类:TreeBin红黑树节点容器
static final class TreeBin<K,V> extends Node<K,V> {
    TreeNode<K,V> root;//红黑树根节点
    volatile TreeNode<K,V> first;//双向链表头节点(first)
    volatile Thread waiter;
    volatile int lockState;
    ...
}

// 内部类:TreeNode节点,继承自Node,用于红黑树结构
static final class TreeNode<K, V> extends Node<K, V> {
    TreeNode<K, V> parent;  // 父节点
    TreeNode<K, V> left;    // 左子节点
    TreeNode<K, V> right;   // 右子节点
    boolean red;            // 节点颜色(红或黑)

    // TreeNode的构造函数及红黑树相关方法等...
}

TreeBin和TreeNode是ConcurrentHashMap中处理红黑树结构的关键组件,它们之间是容器与节点的关系。

TreeBin作为红黑树的容器节点,内部持有指向TreeNode双向链表头节点(first)和红黑树根节点(root)的引用。

TreeNode则代表红黑树中的单个节点,同样继承自Node类,但额外包含红黑树所需的parent、left、right等指针以及颜色标记red。

这种设计实现了TreeBin对红黑树的结构管理和并发控制,而TreeNode专注于存储数据和维护树结构。

java 复制代码
/**
 * The bin count threshold for using a tree rather than list for a
 * bin.  Bins are converted to trees when adding an element to a
 * bin with at least this many nodes. The value must be greater
 * than 2, and should be at least 8 to mesh with assumptions in
 * tree removal about conversion back to plain bins upon
 * shrinkage.
 */
static final int TREEIFY_THRESHOLD = 8;

将链表结构转换为红黑树结构的桶节点数阈值。当向一个桶中添加元素时,如果该桶中的节点数达到或超过此值,就会将链表转换为红黑树。此值必须大于2,并且为了与树结构在收缩时转换回普通桶的假设相协调,该值至少应为8。此时桶位中存储的是TreeBin对象而非TreeNode对象。

扩容节点ForwardingNode

java 复制代码
// 内部类:ForwardingNode节点,用于在扩容时转发请求到新的table
static final class ForwardingNode<K, V> extends Node<K, V> {
    final Node<K, V>[] nextTable; // 指向新的哈希表数组
    // ForwardingNode的构造函数等...
}

在ConcurrentHashMap的扩容过程中,ForwardingNode节点发挥着关键的协调和转发作用。当某个哈希桶内的所有节点都完成数据迁移到新数组(nextTable)后,就会在旧数组的对应桶位放入ForwardingNode节点作为标记。

这个节点本身不存储实际的数据键值对,而是作为一个转发器。当其他线程在查询时遇到ForwardingNode,查询操作会被引导到扩容后的新数组上进行。如果有线程执行put操作时发现当前桶位已是ForwardingNode,该线程会调用helpTransfer()方法协助进行扩容操作,待扩容完成后再将新节点放入新数组的对应位置。作为内部机制的一部分来协调并发操作。

临时节点ReservationNode

java 复制代码
// 内部类:computeIfAbsent and compute时使用
static final class ReservationNode<K,V> extends Node<K,V> {
    ReservationNode() {
        super(RESERVED, null, null);
    }

    Node<K,V> find(int h, Object k) {
        return null;
    }
}

ReservationNode是ConcurrentHashMap中的一种特殊占位符节点,主要在compute()和computeIfAbsent()方法中使用。该节点在正式赋值之前起到暂时占位的作用,和TreeBin、ForwardingNode节点一样,不存储实际的数据键值对。

当执行compute或computeIfAbsent操作时,如果目标桶位为空,ConcurrentHashMap会先创建一个ReservationNode作为临时占位符。这个机制确保了在复杂的函数式计算过程中,其他线程不会同时修改同一个桶位,从而保证了操作的原子性和线程安全性。在计算完成后,这个ReservationNode占位符会被替换为包含实际数据的普通Node节点。作为内部机制的一部分来协调并发操作。

核心方法‌

ConcurrentHashMap的节点类型构成了其并发架构的基础,核心API方法通过这些精心设计的节点体系实现高效的并发控制,主要包括基础数据操作、状态查询和特殊功能三大类控制。

核心操作方法

put() / putVal()方法‌

这是最核心的插入方法,采用CAS+synchronized实现线程安全。当目标桶位为空时,使用CAS操作直接插入新节点;当桶位不为空时,对桶位的头节点加synchronized锁,然后在链表或红黑树中执行插入操作。如果插入后链表长度达到TREEIFY_THRESHOLD(默认8)且数组容量≥64,链表会转换为红黑树结构。

java 复制代码
final V putVal(K key, V value, boolean onlyIfAbsent) {
    // 参数校验:ConcurrentHashMap不允许key或value为null
    if (key == null || value == null) throw new NullPointerException();
    
    // 计算key的哈希值,通过spread方法分散哈希碰撞
    int hash = spread(key.hashCode());
    int binCount = 0;  // 记录链表长度或树节点数
    
    // 无限循环,确保在各种情况下都能完成插入操作
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh; K fk; V fv;
        
        // 情况1:哈希表未初始化
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();  // 初始化哈希表
        
        // 情况2:目标桶位为空
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // 使用CAS无锁操作插入新节点
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
                break;  // 插入成功则退出循环
        
        // 情况3:当前桶位正在扩容迁移
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);  // 协助扩容
        
        // 情况4:检查首节点是否匹配且onlyIfAbsent为true
        else if (onlyIfAbsent // 不加锁检查首节点
                 && fh == hash
                 && ((fk = f.key) == key || (fk != null && key.equals(fk)))
                 && (fv = f.val) != null)
            return fv;  // 直接返回现有值,不进行插入
        
        // 情况5:需要处理哈希冲突
        else {
            V oldVal = null;
            // 对桶位头节点加锁,保证线程安全
            synchronized (f) {
                // 双重检查:确认加锁后头节点未被修改
                if (tabAt(tab, i) == f) {
                    
                    // 子情况5.1:处理链表结构
                    if (fh >= 0) {
                        binCount = 1;  // 链表节点计数从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.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);
                                break;
                            }
                        }
                    }
                    
                    // 子情况5.2:处理红黑树结构
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;  // 树节点计数设为2
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;  // 更新树节点值
                        }
                    }
                    
                    // 子情况5.3:处理ReservationNode(递归更新检测)
                    else if (f instanceof ReservationNode)
                        throw new IllegalStateException("Recursive update");
                }
            }
            
            // 后处理:检查是否需要树化或返回旧值
            if (binCount != 0) {
                // 链表长度达到阈值时进行树化
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);  // 链表转红黑树
                // 如果覆盖了现有值,返回旧值
                if (oldVal != null)
                    return oldVal;
                break;  // 插入成功,退出循环
            }
        }
    }
    
    // 更新元素计数,采用分片计数减少竞争
    addCount(1L, binCount);
    return null;  // 新插入节点返回null
}

CAS插入节点方法

java 复制代码
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                    Node<K,V> c, Node<K,V> v) {
    return U.compareAndSetReference(tab, ((long)i << ASHIFT) + ABASE, c, v);
}

第一个参数表示需要进行原子更新的哈希表对象。

第二个参数表示目标字段在对象中的内存偏移量。

第三个参数表示期望的旧值,传入的是null。

第四个参数表示要设置的新值,也就是新插入的节点。

方法执行流程总结

1.参数校验‌:确保key和value都不为null

2.哈希计算‌:通过spread方法优化哈希分布

3.表初始化‌:延迟初始化哈希表数组

4.空桶处理‌:使用CAS无锁插入

5.扩容协助‌:发现正在扩容时协助迁移

6.快速检查‌:在onlyIfAbsent模式下不加锁检查首节点

7.哈希冲突处理‌:对头节点加锁后处理链表或树

8.数据结构转换‌:必要时进行链表到红黑树的转换

9.计数更新‌:采用分片计数机制

get()方法‌

get方法通过volatile读实现无锁查询。首先根据key的hash值定位到数组索引,然后遍历该桶位的链表或红黑树查找匹配的节点。由于Node的val和next字段都使用volatile修饰,保证了多线程环境下的内存可见性。

java 复制代码
public V get(Object key) {
    // 定义局部变量:tab-哈希表数组,e-当前节点,p-临时节点,n-数组长度,eh-节点哈希值,ek-节点键值
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    
    // 计算key的哈希值,通过spread方法分散哈希冲突
    int h = spread(key.hashCode());
    
    // 步骤1:基础条件检查
    // 检查哈希表已初始化、数组长度有效,并且目标桶位不为空
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        
        // 步骤2:检查首节点是否匹配
        // 比较哈希值,如果哈希值匹配再比较key
        if ((eh = e.hash) == h) {
            // 通过引用相等或equals方法比较key
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;  // 匹配成功,返回节点值
        }
        
        // 步骤3:处理特殊节点(哈希值为负)
        // 当eh < 0时,表示当前节点是特殊节点:TreeBin(哈希值-2)、ForwardingNode(哈希值-1)或ReservationNode(哈希值-3)
        else if (eh < 0)
            // 调用对应节点的find方法进行查找
            // - TreeBin:在红黑树中查找
            // - ForwardingNode:转发到新表中查找
            // - ReservationNode:返回null
            return (p = e.find(h, key)) != null ? p.val : null;
        
        // 步骤4:遍历链表查找
        // 如果首节点不匹配且不是特殊节点,则遍历链表
        while ((e = e.next) != null) {
            // 对每个节点进行哈希值和key的匹配检查
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;  // 找到匹配节点,返回值
        }
    }
    
    // 步骤5:未找到匹配的key,返回null
    return null;
}

方法执行流程总结

1.哈希计算阶段

2.首节点快速检查(这种设计优化了最常见的命中情况)

3.特殊节点处理

4.链表遍历阶段

remove()方法‌

删除操作同样采用synchronized锁住桶位头节点的方式保证线程安全。在删除节点后,如果树结构的节点数减少到UNTREEIFY_THRESHOLD(默认6)以下,红黑树会退化为链表。

状态查询方法

size() / mappingCount()方法‌

size()方法返回int类型的元素个数,而mappingCount()返回long类型,更推荐使用后者以避免整数溢出。

isEmpty()方法‌

通过遍历数组检查是否存在非空桶位来实现,由于不需要精确计数,性能比size()方法更高。

原子更新方法

compute() / computeIfAbsent() / computeIfPresent()方法‌

这些方法支持原子性的函数式更新。在执行过程中,如果目标桶位为空,会先创建ReservationNode作为占位符,待计算完成后替换为实际节点。这种机制确保了在复杂的函数式计算过程中,其他线程不会同时修改同一个桶位。

merge()方法‌

合并操作也采用类似的并发控制机制,当原值不存在时直接插入新值,存在时通过BiFunction合并原值和新值。

扩容相关方法

transfer()方法‌

负责数据迁移的核心扩容方法,采用多线程并发迁移策略。每个线程负责迁移数组的一个分片,迁移完成后在旧桶位中放入ForwardingNode节点标记该桶位已迁移完成。

helpTransfer()方法‌

当其他线程在操作过程中发现当前桶位是ForwardingNode时,会调用此方法协助进行数据迁移。

三种Map的并发性能对比

下面通过一个例子来对比ConcurrentHashMap、HashMap、HashTable的并发性能,这个例子采用10个线程并发往map中添加数据,每个线程添加10万次。观察三种Map的并发添加耗时。

java 复制代码
public class MapConcurrentTest {
    private static final int THREAD_COUNT = 10;
    private static final int OPERATION_COUNT = 100000;

    public static void main(String[] args) throws InterruptedException {
        // 测试三种Map的并发性能
        testHashMap();
        testHashTable();
        testConcurrentHashMap();
    }

    // 测试HashMap(非线程安全)
    public static void testHashMap() throws InterruptedException {
        Map<String, Integer> map = new HashMap<>();
        System.out.println("=== HashMap 测试 ===");
        long startTime = System.currentTimeMillis();

        Thread[] threads = new Thread[THREAD_COUNT];
        for (int i = 0; i < THREAD_COUNT; i++) {
            final int threadId = i;
            threads[i] = new Thread(() -> {
                for (int j = 0; j < OPERATION_COUNT; j++) {
                    String key = "key-" + threadId + "-" + j;
                    map.put(key, j);
                }
            });
            threads[i].start();
        }

        for (Thread thread : threads) {
            thread.join();
        }

        long endTime = System.currentTimeMillis();
        System.out.println("HashMap 最终大小: " + map.size() +
                " (期望: " + (THREAD_COUNT * OPERATION_COUNT) + ")");
        System.out.println("HashMap 耗时: " + (endTime - startTime) + "ms");
        System.out.println();
    }

    // 测试HashTable(线程安全但性能较低)
    public static void testHashTable() throws InterruptedException {
        Map<String, Integer> map = new Hashtable<>();
        System.out.println("=== HashTable 测试 ===");
        long startTime = System.currentTimeMillis();

        Thread[] threads = new Thread[THREAD_COUNT];
        for (int i = 0; i < THREAD_COUNT; i++) {
            final int threadId = i;
            threads[i] = new Thread(() -> {
                for (int j = 0; j < OPERATION_COUNT; j++) {
                    String key = "key-" + threadId + "-" + j;
                    map.put(key, j);
                }
            });
            threads[i].start();
        }

        for (Thread thread : threads) {
            thread.join();
        }

        long endTime = System.currentTimeMillis();
        System.out.println("HashTable 最终大小: " + map.size() +
                " (期望: " + (THREAD_COUNT * OPERATION_COUNT) + ")");
        System.out.println("HashTable 耗时: " + (endTime - startTime) + "ms");
        System.out.println();
    }

    // 测试ConcurrentHashMap(高性能线程安全)
    public static void testConcurrentHashMap() throws InterruptedException {
        Map<String, Integer> map = new ConcurrentHashMap<>();
        System.out.println("=== ConcurrentHashMap 测试 ===");
        long startTime = System.currentTimeMillis();

        Thread[] threads = new Thread[THREAD_COUNT];
        for (int i = 0; i < THREAD_COUNT; i++) {
            final int threadId = i;
            threads[i] = new Thread(() -> {
                for (int j = 0; j < OPERATION_COUNT; j++) {
                    String key = "key-" + threadId + "-" + j;
                    map.put(key, j);
                }
            });
            threads[i].start();
        }

        for (Thread thread : threads) {
            thread.join();
        }

        long endTime = System.currentTimeMillis();
        System.out.println("ConcurrentHashMap 最终大小: " + map.size() +
                " (期望: " + (THREAD_COUNT * OPERATION_COUNT) + ")");
        System.out.println("ConcurrentHashMap 耗时: " + (endTime - startTime) + "ms");
        System.out.println();
    }
}

运行结果:

复制代码
=== HashMap 测试 ===
HashMap 最终大小: 768182 (期望: 1000000)
HashMap 耗时: 277ms

=== HashTable 测试 ===
HashTable 最终大小: 1000000 (期望: 1000000)
HashTable 耗时: 385ms

=== ConcurrentHashMap 测试 ===
ConcurrentHashMap 最终大小: 1000000 (期望: 1000000)
ConcurrentHashMap 耗时: 323ms

从运行结果可以看出,HashMap的最终大小小于期望值,可能出现数据丢失,这证实了其非线程安全的特性。HashTable的运行结果数据一致性得到保证,但执行时间明显长于其他两种实现,这反映了其全局锁机制带来的性能开销。ConcurrentHashMap的运行结果数据一致性得到保证,性能优于HashTable。

相关推荐
cpp_25012 小时前
P1765 手机
数据结构·c++·算法·题解·洛谷
就是ping不通的蛋黄派3 小时前
数据结构与算法—线性表(C++描述)
数据结构·c++
hweiyu003 小时前
数据结构和算法分类
数据结构·算法·分类
AI小云3 小时前
【数据操作与可视化】Pandas数据处理-Series数据结构
开发语言·数据结构·python·numpy·pandas
前端小L3 小时前
图论专题(十六):“依赖”的死结——用拓扑排序攻克「课程表」
数据结构·算法·深度优先·图论·宽度优先
前端小L3 小时前
图论专题(十三):“边界”的救赎——逆向思维解救「被围绕的区域」
数据结构·算法·深度优先·图论
风筝在晴天搁浅4 小时前
代码随想录 738.单调递增的数字
数据结构·算法
Miraitowa_cheems4 小时前
LeetCode算法日记 - Day 108: 01背包
数据结构·算法·leetcode·深度优先·动态规划
大千AI助手4 小时前
平衡二叉树:机器学习中高效数据组织的基石
数据结构·人工智能·机器学习·二叉树·大模型·平衡二叉树·大千ai助手