一、JDK1.8的ConcurrentHashMap的put方法源码

ConcurrentHashMap 是 Java 并发包(java.util.concurrent)中的一个高性能线程安全哈希表实现。在 JDK 1.8 中,ConcurrentHashMap 的 put 方法是其核心方法之一,负责插入键值对并保证线程安全。

以下是 put 方法的详细源码解析,结合并发机制和设计思想

1、put 方法入口

  • 参数:

    • key:键。

    • value:值。

    • onlyIfAbsent:如果为 true,则仅在键不存在时插入

  • 返回值:如果键已存在,返回旧值;否则返回 null

2、putVal 方法解析

putVal 是 put 方法的核心实现,以下是其源码逐段解析:

(1) 参数校验

  • 设计意图:ConcurrentHashMap 不允许 null 键或值,因为在并发场景中,null 可能导致歧义(例如,无法区分“键不存在”和“键对应的值为 null”)

(2) 哈希计算

  • spread() 方法:对原始哈希码进行二次处理,确保哈希值均匀分布

  • 作用:

    • 通过异或高位和低位,减少哈希冲突

    • 通过 & HASH_BITS 确保哈希值为正数(负哈希用于标记特殊节点,如扩容时的 ForwardingNode)

(3) 自旋插入逻辑

  • 关键点:

    • CAS 无锁插入:若桶为空,直接通过 casTabAt 原子操作插入新节点,避免加锁

    • 协助扩容:若发现桶已被标记为 MOVED(ForwardingNode),当前线程会协助迁移数据,提升扩容效率

(4) initTable() 初始化哈希表

initTable() 是 ConcurrentHashMap 在 JDK 1.8 中用于初始化哈希表的核心方法。当 ConcurrentHashMap 第一次插入数据时,如果哈希表尚未初始化,

会调用 initTable() 方法创建并初始化哈希表。以下是 initTable() 方法的详细源码解析。

a、方法签名

b、方法解析

i、检查表是否已初始化

  • 条件:如果 table 为 null 或长度为 0,表示哈希表尚未初始化。

  • 作用:确保哈希表未初始化时才执行初始化逻辑。

ii、检查是否有其他线程正在初始化

  • sizeCtl 的作用:

    • sizeCtl 是 ConcurrentHashMap 的控制状态变量,用于控制初始化和扩容

    • 当 sizeCtl < 0 时,表示有其他线程正在初始化或扩容

  • Thread.yield():让出 CPU,等待其他线程完成初始化

iii、尝试 CAS 设置 sizeCtl 为 -1

  • CAS 操作:通过 CAS 将 sizeCtl 设置为 -1,表示当前线程正在初始化

  • 作用:确保只有一个线程执行初始化逻辑

iV、双重检查

  • 作用:防止其他线程已经完成初始化

iiV、计算初始容量

  • sc 的作用:

    • 如果 sc > 0,表示用户指定了初始容量

    • 否则,使用默认容量 DEFAULT_CAPACITY(默认值为 16)

  • 作用:确定哈希表的初始容量

iiiV、创建哈希表

  • 作用:创建长度为 n 的 Node 数组,并将其赋值给 table

iVi、计算扩容阈值

  • 计算逻辑:

    • n >>> 2 表示 n / 4

    • n - (n >>> 2) 表示 n * 0.75

  • 作用:设置扩容阈值(sizeCtl),当元素数量超过阈值时触发扩容

iVii、 恢复 sizeCtl 的值

  • 作用:初始化完成后,恢复 sizeCtl 的值,表示初始化完成

c、示例场景

假设 ConcurrentHashMap 第一次插入数据:

1、线程 A 检查 table,发现尚未初始化

2、线程 A 通过 CAS 将 sizeCtl 设置为 -1,表示正在初始化

3、线程 A 创建哈希表,并设置扩容阈值

4、线程 A 恢复 sizeCtl 的值,表示初始化完成

5、其他线程在初始化期间会调用 Thread.yield(),等待初始化完成

(5) helpTransfer(tab, f)方法源码

helpTransfer(tab, f) 是 ConcurrentHashMap 在 JDK 1.8 中用于协助扩容的核心方法之一。当线程在插入数据时发现当前桶正在迁移(即桶的头节点是 ForwardingNode),会调用 helpTransfer 方法协助其他线程完成数据迁移。以下是 helpTransfer 方法的详细源码解析:

a、方法签名

b、方法解析

i、 检查扩容状态

  • 条件:

    • 1、当前表 tab 不为空。

    • 2、当前桶的头节点 f 是 ForwardingNode(表示桶正在迁移)。

    • 3、ForwardingNode 的 nextTable 不为空(表示扩容正在进行)。

  • 作用:确认当前表正在扩容,且需要协助。

ii、 计算扩容戳

  • resizeStamp() 方法:生成一个扩容戳,用于标识当前扩容的状态。

  • 作用:

    • 1、Integer.numberOfLeadingZeros(n):计算表长度的前导零个数

    • 2、(1 << (RESIZE_STAMP_BITS - 1)):生成一个标志位

    • 3、最终结果是一个唯一的扩容戳,用于标识当前扩容

iii、 检查扩容是否仍在进行

  • 条件:

    • 1、nextTab 仍然是 nextTable(扩容目标表未改变)。

    • 2、table 仍然是 tab(当前表未改变)。

    • 3、sizeCtl 为负数(表示扩容仍在进行)。

  • 作用:确保当前扩容状态未发生变化

vi、判断是否需要协助

  • 条件:

    • 1、(sc >>> RESIZE_STAMP_SHIFT) != rs:扩容戳不匹配。

    • 2、sc == rs + 1:扩容已完成。

    • 3、sc == rs + MAX_RESIZERS:扩容线程数已达到最大值。

    • 4、transferIndex <= 0:所有桶已分配完毕。

  • 作用:如果满足任一条件,则无需协助,直接退出。

vii、尝试增加扩容线程数

  • CAS 操作:通过 CAS 将 sizeCtl 的值加 1,表示当前线程加入扩容

  • transfer() 方法:调用 transfer 方法协助迁移数据

viii、返回新表

  • 作用:返回扩容目标表 nextTab,供后续操作使用

c、transfer 方法的作用

transfer 方法是 ConcurrentHashMap 扩容的核心逻辑,负责将数据从旧表迁移到新表。以下是 transfer 方法的主要步骤:

  • 1、计算迁移范围:

    • 每个线程负责迁移一部分桶

    • 通过 transferIndex 分配迁移任务

  • 2、迁移数据:

    • 遍历旧表中的每个桶,将节点迁移到新表

    • 如果桶是链表,则拆分为高低链表;如果是红黑树,则拆分为高低树

  • 3、标记迁移完成:

    • 迁移完成后,将桶标记为 ForwardingNode

d、示例场景

假设 ConcurrentHashMap 正在扩容,线程 A 在插入数据时发现当前桶正在迁移,调用 helpTransfer 方法协助迁移:

1、线程 A 检查扩容状态,确认需要协助

2、线程 A 通过 CAS 增加扩容线程数,并调用 transfer 方法迁移数据

3、线程 A 完成迁移任务后,继续执行插入操作

(6) transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) 解析

transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) 是 ConcurrentHashMap 在 JDK 1.8 中用于扩容和数据迁移的核心方法。当哈希表中的元素数量超过阈值时,ConcurrentHashMap 会触发扩容,

并通过 transfer 方法将数据从旧表迁移到新表。transfer 方法支持多线程协作迁移数据,以提高扩容效率。

以下是 transfer 方法的详细源码解析

a、方法签名

    private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
// 1. 计算每个线程负责的迁移范围(stride)
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // 最小迁移范围
// 2. 初始化新表(nextTab)
if (nextTab == null) {
try {
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]; // 新表容量为旧表的两倍
nextTab = nt;
} catch (Throwable ex) {
sizeCtl = Integer.MAX_VALUE; // 扩容失败,恢复 sizeCtl
return;
}
nextTable = nextTab;
transferIndex = n; // 初始化迁移索引
}
int nextn = nextTab.length;
// 3. 创建 ForwardingNode 节点,用于标记迁移中的桶
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true;
boolean finishing = false; // 是否完成迁移
// 4. 自旋迁移数据
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
// 4.1 分配迁移任务
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing)
advance = false;
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
else if (U.compareAndSwapInt(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ? nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
// 4.2 检查迁移是否完成
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) {
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1); // 更新 sizeCtl
return;
}
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
i = n; // 重新检查所有桶
}
}
// 4.3 迁移空桶
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
// 4.4 迁移已处理的桶
else if ((fh = f.hash) == MOVED)
advance = true; // 跳过已迁移的桶
// 4.5 迁移非空桶
else {
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
if (fh >= 0) { // 链表节点
// 拆分为高低链表
int runBit = fh & n;
Node<K,V> lastRun = f;
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.val;
if ((ph & n) == 0)
ln = new Node<K,V>(ph, pk, pv, ln);
else
hn = new Node<K,V>(ph, pk, pv, hn);
}
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
else if (f instanceof TreeBin) { // 红黑树节点
// 拆分为高低树
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> lo = null, loTail = null;
TreeNode<K,V> hi = null, hiTail = null;
int lc = 0, hc = 0;
for (Node<K,V> e = t.first; e != null; e = e.next) {
int h = e.hash;
TreeNode<K,V> p = new TreeNode<K,V>
(h, e.key, e.val, null, null);
if ((h & n) == 0) {
if ((p.prev = loTail) == null)
lo = p;
else
loTail.next = p;
loTail = p;
++lc;
}
else {
if ((p.prev = hiTail) == null)
hi = p;
else
hiTail.next = p;
hiTail = p;
++hc;
}
}
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
(hc != 0) ? new TreeBin<K,V>(lo) : t;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin<K,V>(hi) : t;
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}

b、方法解析

i、计算迁移范围

  • stride:每个线程负责迁移的桶数量

  • NCPU:CPU 核心数

  • MIN_TRANSFER_STRIDE:最小迁移范围(默认 16)

  • 作用:根据 CPU 核心数动态分配迁移任务

ii、初始化新表

  • 作用:创建新表,容量为旧表的两倍

iii、创建 ForwardingNode

  • ForwardingNode:用于标记迁移中的桶,其他线程遇到时会协助迁移

Vi、自旋迁移数据

  • 关键点:

    • 分配迁移任务:通过 CAS 更新 transferIndex,分配迁移范围

    • 迁移空桶:将空桶标记为 ForwardingNode

    • 迁移非空桶:加锁迁移链表或红黑树

Vii、迁移链表

  • 作用:将链表拆分为高低链表,分别迁移到新表

Viii、迁移红黑树

  • 作用:将红黑树拆分为高低树,分别迁移到新表

c、总结

transfer 方法:用于扩容和数据迁移,支持多线程协作。

  • 核心机制:

    • 通过 CAS 分配迁移任务。

    • 使用 ForwardingNode 标记迁移中的桶。

    • 渐进式迁移,不影响读操作。

  • 适用场景:ConcurrentHashMap 的动态扩容

(7) 处理哈希冲突(加锁逻辑)

  • 锁策略:

    • 细粒度锁:仅锁住当前冲突的桶(头节点),其他桶仍可并发访问。

    • 双重检查:加锁后再次检查桶是否被修改,避免锁期间其他线程已修改结构

  • 链表与红黑树:

    • 链表插入采用尾插法(JDK 1.8 的改进,避免循环链表问题)。

    • 当链表长度超过 TREEIFY_THRESHOLD(默认8),会转换为红黑树。

(8) 链表转红黑树

  • 触发条件:单个桶的链表长度 ≥ TREEIFY_THRESHOLD(默认8)。

  • 实际转换条件:

    • 哈希表长度 ≥ MIN_TREEIFY_CAPACITY(默认64),否则优先扩容。

(9) 更新元素计数

  • addCount() 方法:使用类似 LongAdder 的分段计数机制,减少 CAS 竞争。

  • 设计目的:通过分散计数到多个 CounterCell,避免所有线程竞争同一变量

二、核心并发设计总结

1、无锁读:读操作直接访问 volatile 变量,无需同步。

2、CAS 写:桶为空时通过 CAS 插入,避免锁竞争。

3、细粒度锁:哈希冲突时仅锁住当前桶,锁粒度极小。

4、分段计数:使用 CounterCell 分散计数,减少 CAS 冲突。

5、协作扩容:多线程协同迁移数据,提升扩容效率。

JDK1.8的ConcurrentHashMap的put方法源码的更多相关文章

  1. HashMap和ConcurrentHashMap实现原理及源码分析

    HashMap实现原理及源码分析 哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表, ...

  2. 还不懂 ConcurrentHashMap ?这份源码分析了解一下

    上一篇文章介绍了 HashMap 源码,反响不错,也有很多同学发表了自己的观点,这次又来了,这次是 ConcurrentHashMap 了,作为线程安全的HashMap ,它的使用频率也是很高.那么它 ...

  3. HashMap实现原理一步一步分析(1-put方法源码整体过程)

    各位同学大家好, 今天给大家分享一下HashMap内部的实现原理, 这一块也是在面试过程当中基础部分被问得比较多的一部分. 想要搞清楚HashMap内部的实现原理,我们需要先对一些基本的概念有一些了解 ...

  4. Java split方法源码分析

    Java split方法源码分析 public String[] split(CharSequence input [, int limit]) { int index = 0; // 指针 bool ...

  5. erlang下lists模块sort(排序)方法源码解析(二)

    上接erlang下lists模块sort(排序)方法源码解析(一),到目前为止,list列表已经被分割成N个列表,而且每个列表的元素是有序的(从大到小) 下面我们重点来看看mergel和rmergel ...

  6. erlang下lists模块sort(排序)方法源码解析(一)

    排序算法一直是各种语言最简单也是最复杂的算法,例如十大经典排序算法(动图演示)里面讲的那样 第一次看lists的sort方法的时候,蒙了,几百行的代码,我心想要这么复杂么(因为C语言的冒泡排序我记得不 ...

  7. getOrCreateEnvironment()方法源码探究

    该方法目的是创建一个环境对象,并且根据环境类型,自动判断是创建web环境对象,还是标准非web环境对象. 首先该方法源于prepareEnvironment准备环境: 然后进入该方法源码: 可以发现: ...

  8. TreeSet集合的add()方法源码解析(01.Integer自然排序)

    >TreeSet集合使用实例 >TreeSet集合的红黑树 存储与取出(图) >TreeSet的add()方法源码     TreeSet集合使用实例 package cn.itca ...

  9. invalidate和requestLayout方法源码分析

    invalidate方法源码分析 在之前分析View的绘制流程中,最后都有调用一个叫invalidate的方法,这个方法是啥玩意?我们来看一下View类中invalidate系列方法的源码(ViewG ...

  10. Linq分组操作之GroupBy,GroupJoin扩展方法源码分析

    Linq分组操作之GroupBy,GroupJoin扩展方法源码分析 一. GroupBy 解释: 根据指定的键选择器函数对序列中的元素进行分组,并且从每个组及其键中创建结果值. 查询表达式: var ...

随机推荐

  1. Linux blkid命令

    Linux blkid命令:显示块设备属性. Linux blkid命令 功能描述 使用blkid命令可以用来查询系统的块设备(包括交换分区)所使用的文件系统类型.卷标.UUID等信息. Linux ...

  2. uni-app配置顶部标题样式

    在pages.json中,通过配置这个文件,可以去设置当前页面的标题样式, 赋值的时候,将注册删除哈!!! 这样配置兼容 小程序和H5端 在配置的时候,没有给背景色,我还以为在uniapp中不兼容小程 ...

  3. [白话解析] 通俗解析集成学习之GBDT

    [白话解析] 通俗解析集成学习之GBDT 目录 [白话解析] 通俗解析集成学习之GBDT 0x00 摘要 0x01 定义 & 简述 1. GBDT(Gradient Boosting Deci ...

  4. ssh免密登录,服务器互信。

    1.ssh-keygen 产生本主机的公钥和私钥. ssh-keygen -t rsa 文件保存在 ~/.ssh/目录下,其中 id_rsa:本地服务器的私钥 id_rsa.pub:本地服务器的公钥 ...

  5. 支付宝AES如何加密

    继之前给大家介绍了 V3 加密解密的方法之后,今天给大家介绍下支付宝的 AES 加密. 注意:以下说明均在使用支付宝 SDK 集成的基础上,未使用支付宝 SDK 的小伙伴要使用的话老老实实从 AES ...

  6. 面试题54. 二叉搜索树的第k大节点

    地址:https://leetcode-cn.com/problems/er-cha-sou-suo-shu-de-di-kda-jie-dian-lcof/ <?php /** 面试题54. ...

  7. 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本地AI Agent

    一.天价邀请码VS开源革命:打工人今夜无眠 昨夜科技圈被两个关键词刷屏:​Manus激活码炒至5万元5,7,​GitHub神秘项目OpenManus突然开源6,7.这场戏剧性对决的背后,是一场关于「A ...

  8. 【数学公式】mathtype和word2016集成

    mathtype 安装好了以后,word 没有相应的选项卡怎么办? 问题 解决办法 找到word的启动路径 2. 找到mathtype 安装好后的mathpage文件夹 进入文件夹,找到MathPag ...

  9. AI大模型的崛起:从技术突破到行业变革

    在人工智能技术飞速发展的今天,AI大模型作为新一代的智能工具,正逐步渗透到各行各业,引领着数字化转型的新浪潮.前瞻产业研究院发布的一份关于AI大模型场景应用的报告显示,2023年,我国AI大模型行业规 ...

  10. mac 如何开启指定端口供外部访问?

    前言 需要 mac 上开放指定端口,指定 ip 访问 解决 在 macOS 上开放一个端口,并指定只能特定的 IP 访问,可以使用 macOS 内置的 pfctl(Packet Filter)工具来实 ...