ConcurrentHashMap详解

ConcurrentHashMap 深度解析(基于 JDK 8)

ConcurrentHashMap 是 Java 并发包(java.util.concurrent)中提供的线程安全的 Map 实现,专为高并发场景设计。它解决了传统 Hashtable 全表锁效率低下的问题,通过细粒度的同步机制(JDK 8 中为 CAS + synchronized)实现高效并发,是多线程环境下键值对存储的首选。

ConcurrentHashMap 核心特性

线程安全

通过细粒度同步机制(而非全表锁)保证多线程读写安全,不同桶(数组索引)的操作可并发执行,大幅提升并发效率。

高效并发

  • 读操作(get)无锁(依赖 volatile 保证可见性),性能接近 HashMap
  • 写操作(putremove)仅锁定哈希冲突的链表 / 红黑树头节点,不阻塞其他桶的操作。

不允许 null 键值

键(key)和值(value)均不能为 null,避免与 get 方法返回 null 的歧义(无法区分 “键不存在” 和 “值为 null”)。

支持原子操作

提供 putIfAbsentremove(带条件)、replace 等原子操作,无需额外同步即可实现线程安全的复合逻辑(如 “不存在则插入”“存在则更新”)。

弱一致性迭代器

迭代器遍历期间允许其他线程修改集合,不会抛出 ConcurrentModificationException(弱一致性),但可能无法实时反映最新修改。

底层结构与并发机制演变

ConcurrentHashMap 的底层结构随 JDK 版本演进,核心差异在于锁机制:

JDK 7:分段锁(Segment)

  • 结构:整体为 Segment 数组(默认 16 个),每个 Segment 是一个独立的哈希表(类似 HashMap),包含 HashEntry 数组 + 链表。
  • 锁机制:每个 Segment 独立加锁(ReentrantLock),不同 Segment 的操作可并发执行,锁粒度为 “段”。
  • 局限:段数固定(默认 16),并发度受限;扩容仅针对单个 Segment,复杂且效率较低。

JDK 8:CAS + synchronized(主流版本)

彻底废弃 Segment,采用与 HashMap 类似的 “数组 + 链表 / 红黑树” 结构,但通过更细粒度的同步实现并发安全:

  • 数组Node<K,V>[] table,存储键值对,每个元素为链表 / 红黑树的头节点。
  • 链表 / 红黑树:解决哈希冲突,链表长度超过 8 且数组容量 ≥ 64 时转为红黑树。
  • 锁机制:
    • 对空桶(索引位置为 null)使用 CAS 原子操作插入节点;
    • 对非空桶(存在哈希冲突)使用 synchronized 锁定头节点,仅阻塞同桶的并发操作。

核心结构:节点与数组

核心节点类型

JDK 8 中通过不同类型的节点实现并发控制和数据存储:

  • Node<K,V>:基础节点,存储键值对,valuevolatile 修饰,保证多线程可见性。

    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; // 链表后继节点,volatile 保证链表结构可见性
        // ... 构造器和方法
    }
  • TreeNode<K,V>:红黑树节点,继承 Node,增加树结构的父子节点引用,用于哈希冲突严重时提升查询效率。

  • ForwardingNode<K,V>:扩容时的转发节点,标记当前桶已迁移至新数组,引导线程协助扩容。

核心变量

// 哈希表数组(volatile 修饰,保证扩容后可见性)
transient volatile Node<K,V>[] table;

// 扩容时的新数组(volatile 修饰)
private transient volatile Node<K,V>[] nextTable;

// 元素数量(通过 CAS 原子更新)
private transient volatile long baseCount;

// 控制扩容的标记(负值表示正在扩容,正值为下次扩容阈值)
private transient volatile int sizeCtl;

// 加载因子(默认 0.75,与 HashMap 一致)
private static final float LOAD_FACTOR = 0.75f;

核心方法解析(JDK 8)

1. put(K key, V value):线程安全插入

put 方法通过 CAS 和 synchronized 实现并发插入,核心逻辑如下:

public V put(K key, V value) {
    return putVal(key, value, false);
}

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException(); // 禁止 null 键值
    int hash = spread(key.hashCode()); // 二次哈希(与 HashMap 类似,增强分布均匀性)
    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 保证线程安全)
        }
        // 1. 计算索引,若桶为空,用 CAS 插入新节点
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null, new Node<>(hash, key, value, null))) {
                break; // CAS 成功,插入完成
            }
        }
        // 2. 若桶正在扩容(头节点为 ForwardingNode),协助扩容
        else if ((fh = f.hash) == MOVED) {
            tab = helpTransfer(tab, f);
        }
        // 3. 桶非空且未扩容,用 synchronized 锁定头节点处理哈希冲突
        else {
            V oldVal = null;
            synchronized (f) { // 锁定头节点,仅阻塞同桶操作
                if (tabAt(tab, i) == f) { // 再次校验头节点(防止扩容后变化)
                    if (fh >= 0) { // 链表节点
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            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<>(hash, key, value, null);
                                break;
                            }
                        }
                    }
                    else if (f instanceof TreeBin) { // 红黑树节点
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent) p.val = value; // 覆盖值
                        }
                    }
                }
            }
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD) { // 链表长度达标,转为红黑树
                    treeifyBin(tab, i);
                }
                if (oldVal != null) return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount); // 原子更新元素数量,可能触发扩容
    return null;
}

并发控制关键点

  • 空桶插入用 CAS 避免加锁,提升效率;
  • 非空桶用 synchronized 锁定头节点,仅阻塞同桶的并发写入,不同桶可并行;
  • 扩容时通过 ForwardingNode 引导其他线程协助扩容,加速扩容过程。

2. get(Object key):无锁读操作

读操作全程无锁,依赖 volatile 保证节点值的可见性:

public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    int hash = spread(key.hashCode()); // 计算哈希值
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & hash)) != null) { // 定位桶(volatile 读)
        if ((eh = e.hash) == hash) { // 头节点匹配
            if ((ek = e.key) == key || (ek != null && key.equals(ek))) {
                return e.val;
            }
        }
        else if (eh < 0) { // 头节点为红黑树或扩容转发节点
            return (p = e.find(hash, key)) != null ? p.val : null;
        }
        while ((e = e.next) != null) { // 遍历链表
            if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) {
                return e.val;
            }
        }
    }
    return null;
}

无锁原理

  • 数组 table 和节点 val 均用 volatile 修饰,保证修改后对其他线程可见;
  • 读操作无需加锁,仅通过 volatile 语义获取最新值,性能接近 HashMap

3. 扩容机制(transfer

当元素数量超过阈值(capacity * loadFactor)时触发扩容,容量翻倍,过程支持多线程协作:

  1. 初始化新数组:通过 CAS 将 sizeCtl 设为负值(标记扩容中),创建新数组(容量为原数组的 2 倍)。
  2. 迁移数据:原数组中的每个桶由一个线程负责迁移,通过 synchronized 锁定桶的头节点,避免并发修改。
  3. 协助扩容:其他线程发现扩容标记(ForwardingNode)时,会主动参与迁移,加速过程。
  4. 替换数组:迁移完成后,用新数组替换原数组,更新 sizeCtl 为新阈值。

原子操作与典型应用

1. 原子操作(ConcurrentMap 接口定义)

  • putIfAbsent(K key, V value):若键不存在则插入,返回旧值(null 表示插入成功),常用于 “初始化唯一实例” 场景(如缓存加载)。
  • remove(Object key, Object value):仅当键关联的值与给定值匹配时才删除,返回是否成功。
  • replace(K key, V oldVal, V newVal):仅当键关联的值为 oldVal 时才更新为 newVal,返回是否成功。

2. 典型应用场景

  • 高并发缓存:如服务器本地缓存,多线程读写共享数据(如用户会话、配置信息)。
  • 共享计数器:用 putIfAbsent 初始化计数器,replace 原子更新计数。
  • 避免重复计算:通过 putIfAbsent 保证同一键对应的 value 仅被计算一次(如懒加载场景)。

与其他 Map 的对比

特性 ConcurrentHashMap HashMap Hashtable
线程安全 是(细粒度锁) 是(全表锁)
并发效率 高(读写可并发) 无并发控制 低(全表锁阻塞)
null 键值 不允许 允许(key 最多 1 个) 不允许
底层结构(JDK 8) 数组 + 链表 / 红黑树 数组 + 链表 / 红黑树 数组 + 链表
锁机制 CAS + synchronized synchronized(全表)
适用场景 高并发读写 单线程 / 无并发 旧系统兼容