ConcurrentHashMap详解

402 阅读4分钟

ConcurrentHashMap通过CAS和细粒度锁(JDK8+)实现高并发线程安全,支持高效读写与动态扩容,适用于缓存、计数器等高并发场景,相比Hashtable性能更优。

一、核心特性

  • 线程安全:高并发场景下性能接近非同步的 HashMap
  • 分段锁(JDK7) :将数据分段(Segment),每个段独立加锁。
  • CAS + synchronized(JDK8+) :细粒度锁(桶级别)和无锁化操作。
  • 支持高并发读写:读操作通常无锁,写操作锁粒度细化。
  • 不允许 null 键或值:避免歧义(如 get(key) 返回 null 时无法区分“不存在”或“值为 null”)。

二、底层数据结构演进

JDK版本实现方式锁机制性能优化
JDK7分段锁(Segment 数组)每个 Segment 继承 ReentrantLock段级别锁,减少竞争
JDK8+数组 + 链表/红黑树 + CASsynchronized 锁桶头节点 + CAS锁粒度更细,并发度更高

三、JDK8+ 核心实现原理

1. 数据结构
  • 数组Node<K,V>[] table,每个桶存储链表或红黑树。

  • 链表节点

    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        volatile V val;         // 值用 volatile 保证可见性
        volatile Node<K,V> next; // 链表指针
    }
    
  • 红黑树节点

    static final class TreeNode<K,V> extends Node<K,V> {
        TreeNode<K,V> parent;  // 父节点
        TreeNode<K,V> left;    // 左子树
        TreeNode<K,V> right;   // 右子树
        boolean red;           // 颜色标记
    }
    
2. 关键机制
  • CAS(Compare-And-Swap) :用于无锁化初始化、节点插入等操作。

    // 使用 Unsafe 类实现 CAS
    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);
    }
    
  • 锁细化:写操作仅锁定当前桶的头节点(链表或红黑树)。

  • 多线程协助扩容:线程在操作时发现正在扩容,会协助迁移数据。


四、核心操作流程

1. put() 流程
ThreadConcurrentHashMapput(key, value)计算 key 的 hash定位桶下标CAS 插入新节点协助扩容synchronized 锁住头节点遍历链表/红黑树更新值插入新节点树化alt[链表长度≥8]alt[Key 存在]alt[桶正在迁移(Forw-ardingNode)]alt[桶为空][桶非空]检查是否需要扩容返回旧值(或 null)ThreadConcurrentHashMap
2. get() 流程
  • 无锁读取:直接访问 volatile 字段,保证可见性。
  • 处理扩容:若遇到 ForwardingNode,转到新数组查找。
3. 扩容机制(transfer()
  • 触发条件:元素数量超过 sizeCtl 阈值。

  • 多线程协作

    • 每个线程处理一个步长(默认 16)的桶区间。
    • 通过 ForwardingNode 标记正在迁移的桶。
  • 迁移规则

    // 例如旧容量为 16,新容量为 32
    if ((e.hash & oldCap) == 0) {
        // 新索引 = 原位置
    } else {
        // 新索引 = 原位置 + oldCap
    }
    

五、线程安全实现对比

实现方式锁粒度并发性能适用场景
Hashtable全局锁(方法级)低并发遗留代码
Collections.synchronizedMap全局锁(对象级)简单同步需求
ConcurrentHashMap(JDK7)段级别锁中等并发读写
ConcurrentHashMap(JDK8+)桶级别锁极高高并发读写、大规模数据场景

六、关键源码解析(JDK17)

1. putVal() 核心代码(简化)
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            tab = initTable(); // CAS 初始化数组
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
                break; // CAS 插入成功
        }
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f); // 协助扩容
        else {
            synchronized (f) { // 锁住头节点
                // 处理链表或红黑树插入
            }
        }
    }
    addCount(1L, binCount); // 更新计数,检查扩容
    return null;
}
2. size() 实现
  • 基础计数:使用 LongAdder 思想,通过 CounterCell[] 分段计数。

  • 最终一致性size() 返回近似值(非精确值),适合高并发场景。

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

七、适用场景与最佳实践

1. 推荐场景
  • 高并发计数器

    ConcurrentHashMap<String, Long> counter = new ConcurrentHashMap<>();
    counter.compute(key, (k, v) -> (v == null) ? 1 : v + 1);
    
  • 缓存系统:如商品信息缓存,支持高频读取和低频更新。

  • 实时数据处理:多线程写入事件数据,保证线程安全。

2. 避免场景
  • 需要强一致性的 size() :使用 AtomicLong 替代。
  • 频繁全表扫描:迭代器弱一致性,可能遗漏或重复数据。
3. 性能优化技巧
  • 初始化容量:避免扩容开销。

    new ConcurrentHashMap<>(32); // 初始容量32
    
  • 避免长链表:优化键的哈希函数,减少冲突。

  • 合理选择并发级别(JDK7):

    new ConcurrentHashMap(16, 0.75f, 8); // 初始容量16,并发级别8
    

八、与其他并发容器对比

容器线程安全实现有序性适用场景
ConcurrentHashMap桶锁 + CAS无序高频键值操作
ConcurrentSkipListMap跳表(无锁读,CAS写)有序需要排序或范围查询
Hashtable全局锁无序遗留代码
Collections.synchronizedMap对象锁无序简单同步需求

九、总结

  • 核心优势:通过分段锁(JDK7)和 CAS + synchronized(JDK8+)实现高并发读写。
  • 数据结构:数组 + 链表/红黑树,支持动态扩容与树化优化。
  • 适用场景:高并发键值存储、计数器、缓存系统。
  • 最佳实践:合理初始化容量、优化哈希函数、避免锁竞争热点。