Java ConcurrentHashMap源码深度解析:从底层原理到性能优化

Java ConcurrentHashMap源码深度解析:从底层原理到性能优化

引言

ConcurrentHashMap 是 Java 并发编程中非常核心的类,它在保证线程安全的同时,提供了极高的并发性能。与 Hashtable 相比,ConcurrentHashMap 通过分段锁(Segment)或更先进的 CAS + Synchronized 机制,避免了全局锁带来的性能瓶颈。本文将从源码级别深入剖析 ConcurrentHashMap 的设计思想、数据结构、核心方法实现以及性能优化策略。


1. 历史演进:从 JDK 7 到 JDK 8

1.1 JDK 7: Segment 分段锁机制

JDK 7 中的 ConcurrentHashMap 采用 分段锁(Segment)的设计。其核心思想是将整个哈希表划分为多个段(Segment),每个段独立加锁,从而允许多个线程同时操作不同的段,提升并发能力。

  • 数据结构Segment<K,V>[] segments,每个 Segment 实际上是一个小型的 HashEntry 表。
  • 锁粒度:锁作用于 Segment,而非整个 Map。
  • 缺点:虽然提升了并发性,但依然存在锁竞争和扩容时的性能问题。

1.2 JDK 8: CAS + Synchronized + Node 数组 + 链表/红黑树结构

JDK 8 对 ConcurrentHashMap 进行了重大重构,摒弃了 Segment,引入了更高效的 CAS 操作 + synchronized 锁 + 红黑树 的组合。

  • 数据结构Node<K,V>[] table,数组 + 链表 + 红黑树。
  • 锁粒度 :锁作用于 桶(bucket),即链表或红黑树的头节点。
  • 核心优势:锁的粒度更细,极大减少了锁竞争,提升了高并发下的性能。

2. 核心数据结构解析(JDK 8)

2.1 Node 结构

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;
    }
}
  • hash: 键的哈希值,用于定位桶位置。
  • key: 键,不可变。
  • val: 值,使用 volatile 保证可见性。
  • next: 指向下一个节点,构成链表。

2.2 TreeNode 与 TreeBin

当链表长度超过阈值(默认为 8),会转换为红黑树以提高查找效率。

java 复制代码
static final class TreeNode<K,V> extends Node<K,V> {
    // 红黑树节点结构,包含 parent, left, right 等指针
}

static final class TreeBin<K,V> {
    TreeNode<K,V> root;
    // 其他字段...
}

3. 核心方法源码分析(JDK 8)

3.1 put(K key, V value) - 插入操作

3.1.1 步骤分解
  1. 计算 hashhash = spread(key.hashCode()),防止哈希冲突。
  2. 判断是否需要初始化 :如果 table == null,调用 initTable() 进行初始化。
  3. 定位桶位置i = (n - 1) & hash,其中 n 是 table 的长度。
  4. CAS 检查空桶 :若桶为空,使用 casTabAt(table, i, null, new Node<>(hash, key, value, null)) 原子插入。
  5. 处理链表/红黑树:如果桶不为空,且是链表,则遍历链表;如果是红黑树,则调用红黑树插入方法。
  6. 同步锁 :对桶的头节点加 synchronized 锁,确保修改的原子性。
  7. 扩容检查 :如果插入后元素数量超过阈值,触发 transfer() 扩容。
3.1.2 CAS 与 Synchronized 的协同
  • CAS:用于无锁场景(如空桶插入)。
  • Synchronized:用于有竞争的场景(如链表插入),锁住的是头节点,而不是整个 map。

关键点:锁的粒度是「桶」,而非「整个 map」,这是性能提升的核心。

3.2 get(K key) - 读取操作

java 复制代码
public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e; int n, eh; K ek;
    int h = spread(key.hashCode());
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        if ((eh = e.hash) == h && ((ek = e.key) == key || (ek != null && ek.equals(key))))
            return e.val;
        while ((e = e.next) != null) {
            if (e.hash == h && ((ek = e.key) == key || (ek != null && ek.equals(key))))
                return e.val;
        }
    }
    return null;
}
  • 特点完全无锁 !读操作不加任何锁,仅依赖 volatile 保证可见性。
  • 优势:读操作性能极高,几乎不受并发影响。

3.3 resize() / transfer() - 扩容机制

  • 并发扩容 :不同于 HashMapConcurrentHashMap 支持并发扩容。
  • 工作方式 :多个线程可以共同参与扩容,将旧 table 中的元素迁移到新 table,通过 sizeCtl 变量协调。
  • 避免死锁 :使用 CAS 检查并设置 sizeCtl 来控制扩容状态。

4. 性能优化策略总结

优化点 说明
细粒度锁 锁作用于桶头节点,极大减少锁竞争
CAS 无锁操作 在无竞争时,通过 CAS 实现原子更新
读操作无锁 读取不加锁,利用 volatile 保证可见性
红黑树优化 链表过长时转为红黑树,降低查找时间复杂度
并发扩容 多线程协作完成扩容,避免阻塞

5. 使用建议与注意事项

  • 不要在遍历时修改集合ConcurrentHashMap 不支持在迭代时修改,否则会抛出 ConcurrentModificationException
  • 避免键或值为 null :虽然允许,但可能导致 get() 返回 null 无法区分是不存在还是值为 null。
  • 合理设置初始容量:可减少扩容次数,提升性能。
  • 优先使用 computeIfAbsent 等原子操作:避免手动同步代码块。

总结

ConcurrentHashMap 是 Java 并发编程中的典范之作。它通过 分段锁 → CAS+Synchronized 的演进,实现了高性能与线程安全的完美平衡。理解其源码不仅有助于我们写出更高效的并发代码,也为我们学习其他并发容器(如 ConcurrentSkipListMap)打下坚实基础。

推荐 :结合 JDK 8 源码阅读,重点关注 put, get, resize 方法的实现细节。

相关推荐
盖世英雄酱581364 小时前
不是所有的this调用会导致事务失效
java·后端
少许极端4 小时前
Redis入门指南(五):从零到分布式缓存-其他类型及Java客户端操作redis
java·redis·分布式·缓存
宠..5 小时前
优化文件结构
java·服务器·开发语言·前端·c++·qt
sheji34165 小时前
【开题答辩全过程】以 疫情物资捐赠系统为例,包含答辩的问题和答案
java
sinat_255487815 小时前
InputStream/OutputStream小讲堂
java·数据结构·算法
乌日尼乐5 小时前
【Java基础整理】java数组详解
java·后端
tkevinjd5 小时前
IO流6(转换流、序列化与反序列化流)
java
虫小宝5 小时前
导购类电商平台搜索推荐融合:基于用户行为的个性化导购系统
java
微露清风6 小时前
系统性学习C++-第十六讲-AVL树实现
java·c++·学习
Hui Baby6 小时前
saga json文件阅读
java·前端·数据库