ConcurrentHashMap源码分析

什么是ConcurrentHashMap

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

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

ConcurrentHashMap的演变

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

在JDK 1.8以及之后,采用CAS加synchronized精细化锁。

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;
}

ConcurrentHashMap和HashMap中的Node有点不同,就在于val和next属性使用了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高性能读的关键。不需要加锁 ,因为节点的val和next都是 volatile的,保证了可见性 。 根据哈希定位到桶,然后遍历链表或搜索红黑树即可。如果遇到ForwardingNode,说明正在扩容,会调用其 find方法到新数组中去查找。

相关推荐
程序猿DD17 分钟前
OctaFuse Gateway 2.15.0:MCP 聚合、向量 Embeddings 与官方目录同步
后端·agent
蛋蛋的就会顺顺的3 小时前
hot100 p0——栈,贪心算法,动态规划
java·前端·javascript·算法·leetcode·力扣
余槐i3 小时前
PostgreSQL 函数索引报错 must be immutable:3种绕法与代价
数据库·后端·sql·postgresql·性能优化
需要8263 小时前
Spring Bean 生命周期:从实例化到销毁的完整钩子
java·后端·spring
小 黄 鸡3 小时前
HashMap 面试必会知识点详解(JDK 8)
java·面试·职场和发展
凤山老林4 小时前
Spring Boot 接入国密 SM2/SM3/SM4 的一些实践记录
spring boot·后端·python
伯恩bourne5 小时前
Spring Boot Bean 注册的两种方式:@Component 家族 vs @Configuration + @Bean
java·spring boot·后端
张彦峰ZYF5 小时前
停顿短不等于延迟低:G1、ZGC 与低延迟服务的目标分野、代价账本与选型标定
后端·回收暂停、应用停止总时长·到达安全点、分配停滞·屏障吃吞吐、并发线程吃核心·并发窗口吃堆余量·分区式靠挑回收集兑现目标·染色指针式不支持压缩引用
silvamux_5 小时前
curl 测试 OpenAI 兼容 API 的完整流程
java·人工智能·缓存·ai·oneapi
pride.li6 小时前
第03章 Python 教程-基础语法:注释与变量
服务器·前端·后端·python·算法·计算机视觉