ConcurrentHashMap源码分析

什么是ConcurrentHashMap

在并发环境下,直接使用 HashMap会导致数据不一致甚至死循环。而使用 Collections.synchronizedMap()包装的Map,虽然线程安全,但它是通过在整个对象上使用 粗粒度锁(synchronized) ​ 来实现的,性能极差(所有操作串行化)。

因此,需要一种支持高并发读写、且保证线程安全的 Map。这就是 ConcurrentHashMap的使命。

ConcurrentHashMap的演变

JDK 1.7以及之前,ConcurrentHashMap由一组Segment(段)组成,每个Segment本质上是一个独立的ReentrantLock和一个小型的HashEntry数组。

JDK 1.8以及之后,采用CASsynchronized精细化锁。

ConcurrentHashMap的底层数据结构

节点类

普通节点

java 复制代码
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;
}

ConcurrentHashMapHashMap中的Node有点不同,就在于valnext属性使用了volatile进行修饰。

红黑树节点

java 复制代码
 static final class TreeNode<K,V> extends Node<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
}

Forwading节点

java 复制代码
static final class ForwardingNode<K,V> extends Node<K,V> {
        final Node<K,V>[] nextTable;
}

参数

sizeCtl

负数:表示初始化或扩容状态

  1. -1 : 表示哈希表 table正在初始化中。这个状态由某个线程通过 CAS 操作独占。

  2. -(1 + n) : 表示哈希表 table正在扩容中。

高 16 位: 存放一个扩容标识戳 (resizeStamp),由当前 table 的长度 n计算得来,用于标记本次扩容是基于哪个长度的 table 进行的。

低 16 位: 表示正在进行扩容的线程数 + 1 。例如,-(1 + 2)表示有 2 个线程正在协助扩容。

零或正数:

  1. 0 : 创建 ConcurrentHashMap 时未指定初始容量,使用默认初始容量(16)。
  2. 正数
    • 在初始化之前 : 表示用户指定的初始容量(会调整为 2 的幂次)。
    • 在初始化或扩容完成之后 : 表示下一次触发扩容的阈值capacity * loadFactor)。

put操作流程

java 复制代码
    public V put(K key, V value) {
        return putVal(key, value, false);
    }

实际的工作委托给putVal函数,第三个参数false表示如果键已存在,则替换。

putVal的内部,整体是一个巨大的自旋循环

java 复制代码
for (Node<K,V>[] tab = table;;) {
 // 无限循环直到插入成功
}
  1. ConcurrentHashMap是懒加载的,因此如果底层的数组是空的,则先进行数组初始化
java 复制代码
if (tab == null || (n = tab.length) == 0)
 tab = initTable();
  1. 如果底层数组不为空,但是当前插入的桶为空,使用CAS插入数据,如果成功则直接插入,如果失败则循环重试。
java 复制代码
if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
    if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
        break;                   // no lock when adding to empty bin
}
  1. 如果当前插入的桶的头是一个ForwardingNode,说明正在扩容,因此线程帮助扩容
java 复制代码
if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
  1. 如果不是第2种和第3种情况,将会使用synchronized锁住该桶头,在该桶内,查找对应的键,如果找到则更新值,如果没有找到则插入尾部。
java 复制代码
else {
    V oldVal = null;
    synchronized (f) { // 锁住链表头节点
        // 双重检查
        if (tabAt(tab, i) == f) {
            // 插入逻辑
        }
    }
}
  1. 插入后,如果达到阈值,则转成红黑树
  2. 更新元素个数,采用类似LongAdder的分段计数的思想。

初始化

ConcurrentHashMap中初始化底层数组时,依旧使用自旋+CAS的方法处理

java 复制代码
while ((tab = table) == null || tab.length == 0){
 ...
}
  1. 如果当前有其他的线程正在初始化,那么将放弃CPU
java 复制代码
if ((sc = sizeCtl) < 0)
             Thread.yield(); // lost initialization race; just spin
  1. 如果没有其他线程在初始化,那么cas设置当前的状态标签为-1
java 复制代码
else if (U.compareAndSetInt(this, SIZECTL, sc, -1))
  1. 竞争成功后,进行双重检查,然后初始化数组,最后恢复状态标签
java 复制代码
try {
 if ((tab = table) == null || tab.length == 0) { // 双重检查
     int n = (sc > 0) ? sc : DEFAULT_CAPACITY;  // 确定初始容量
     Node<K,V>[] nt = new Node<?,?>[n];         // 创建数组
     table = tab = nt;                          // volatile 写入
     sc = n - (n >>> 2);                       // 计算扩容阈值:n*0.75
 }
} finally {
 sizeCtl = sc;  // 恢复为正的阈值
}

get流程

get操作是ConcurrentHashMap高性能读的关键。不需要加锁 ,因为节点的valnext都是 volatile的,保证了可见性 。 根据哈希定位到桶,然后遍历链表或搜索红黑树即可。如果遇到ForwardingNode,说明正在扩容,会调用其 find方法到新数组中去查找。

相关推荐
腻害兔1 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
GetcharZp2 小时前
抛弃低效SSH!这套Ansible+Web可视化神器,让你天天准点下班!
后端
码智社2 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
萧瑟余晖3 小时前
JDK 26 新特性详解
java·开发语言
马优晨4 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
weixin_446260855 小时前
HACO:面向动态部署环境的对冲式智能计算可靠多智能体调度框架
后端·python·flask
ttwuai7 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
用户8356290780517 小时前
Python 实现 Excel 页面布局与打印设置自动化
后端·python
维天说7 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json