ConcurrentHashMap 存储机制(源码解析)

源码系列开更啦 🥰🥰

目录

1. 初始化

1.1. 无参

1.2. 带参

2. 存储操作

2.1. 计算下标

2.2. 初始化数组

2.3. 将数据插入到数组中

2.4. 协作扩容

2.5. 将数据插入到链表

2.6. 将数据插入到红黑树

2.7. 链表树化操作


1. 初始化

1.1. 无参

如果初始化不带参数的话,就什么都没有,其实也是一种延迟初始化的思想

在 后续的 putVal() 方法中,如果哈希表尚未初始化,则调用 initTable() 进行初始化

public ConcurrentHashMap() {// 空的构造函数,没有初始化哈希表数组 
}

1.2. 带参

带参初始化流程

  1. 参数校验:首先检查 initialCapacity 是否为负数,若为负数则抛出异常。
  2. 计算实际容量:根据传入的 initialCapacity,计算哈希表的实际容量 cap。
    1. 如果 initialCapacity 很大,接近最大容量的阈值,则直接将容量设置为 MAXIMUM_CAPACITY;
    2. 否则,将容量设置为比 initialCapacity 稍大的 2 的幂次方,以确保哈希表的效率。
  3. 设置 sizeCtl:将计算得到的容量 cap 赋值给 sizeCtl,用于后续哈希表的初始化。这里的 sizeCtl 代表的是扩容的阈值,后续数组初始化时会变成sizeCtl = sizeCtl - (sizeCtl >>> 2),即 0.75 * sizeCtl

sizeCtl 是 ConcurrentHashMap 中用于控制哈希表在初始化扩容过程中的一个关键变量。它的值在不同阶段具有不同的含义:

  • -1:表示当前有线程正在初始化哈希表。
  • < -1:表示哈希表正在扩容(例如,-2 表示有 1 个线程在执行扩容,-3 表示有 2 个线程在执行扩容)。
  • 0:表示哈希表还未初始化。
  • > 0:表示哈希表的扩容阈值,或者在初始化阶段,用于表示哈希表的初始容量

tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1)

  • 1.5 倍扩展:提供了额外的空间,减少了扩容的频率,提高了性能
  • + 1:确保在一些边界条件下,计算出的容量不会过小,特别是当 initialCapacity 较小的时候
  • 2 的幂次方:确保哈希表能够通过位运算高效地计算索引,避免取模运算,提高哈希表的查找性能。
public ConcurrentHashMap(int initialCapacity) {// 小于直接抛异常if (initialCapacity < 0)throw new IllegalArgumentException();// 如果 initialCapacity 大于等于 MAXIMUM_CAPACITY >>> 1(即接近上限)// 那么直接将容量设置为 MAXIMUM_CAPACITY,防止容量超过最大值// 小于则使用距离 initialCapacity 最近的2次幂int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?MAXIMUM_CAPACITY :tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));this.sizeCtl = cap;
}

具体计算最接近的 2 次幂过程

private static final int tableSizeFor(int c) {int n = c - 1;n |= n >>> 1;n |= n >>> 2;n |= n >>> 4;n |= n >>> 8;n |= n >>> 16;return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}具体计算过程(可忽略)
int n = c - 1;
将传入的容量减 1,用于确保如果传入的容量已经是 2 的幂次方,计算结果不会超过这个数。
例如如果不减一的话,如果是 cap = 2,得到的会是4n = 4
n |= n >>> 1;  // n = 4 | (4 >>> 1) = 4 | 2 = 6
n |= n >>> 2;  // n = 6 | (6 >>> 2) = 6 | 1 = 7
n |= n >>> 4;  // n = 7 | (7 >>> 4) = 7 | 0 = 7
n |= n >>> 8;  // n = 7 | (7 >>> 8) = 7 | 0 = 7
n |= n >>> 16; // n = 7 | (7 >>> 16) = 7 | 0 = 7n = 9;                  // 二进制 1001
n |= n >>> 1;           // n = 9 | (9 >>> 1) = 9 | 4 = 13   // 二进制 1101
n |= n >>> 2;           // n = 13 | (13 >>> 2) = 13 | 3 = 15 // 二进制 1111
n |= n >>> 4;           // n = 15 | (15 >>> 4) = 15 | 0 = 15 // 二进制 1111
n |= n >>> 8;           // n = 15 | (15 >>> 8) = 15 | 0 = 15 // 二进制 1111
n |= n >>> 16;          // n = 15 | (15 >>> 16) = 15 | 0 = 15 // 二进制 1111最后 return n + 1; 就是最接近的 2 次幂

2. 存储操作

public V put(K key, V value) {// 在调用put方法时,会调用putVal,第三个参数默认传递为false// 为false,putVal 将直接插入新的键值对,如果键已经存在,则更新其对应的值// 为true,putVal 只会在键不存在时插入新值,如果键已经存在,则不会更新已有的值return putVal(key, value, false);
}

2.1. 计算下标

final V putVal(K key, V value, boolean onlyIfAbsent) {// ConcurrentHashMap 是不允许存放空值的if (key == null || value == null) throw new NullPointerException();// 计算key的hash值int hash = spread(key.hashCode());// 记录哈希桶节点的个数int binCount = 0;// ... 这里省略,后续详细讲
}

  • h ^ (h >>> 16)
    • 通过 h ^ (h >>> 16),将哈希值的高位与低位混合,确保高位的信息也会影响哈希表的索引计算结果。这样可以减少哈希碰撞,提升哈希表的性能。
    • 因为后续会用到 (n - 1) & h 来计算在 table 的槽位是哪个,如果高16位不与低16位^运算的话,那么基本高位永远也参加不了计算
  • & HASH_BITS
    • 确保哈希值为非负数
    • ConcurrentHashMap 使用负数来表示某些特殊状态(如扩容、红黑树等),负数哈希值可能会与这些标志值冲突(后续的(fh = f.hash) == MOVED)。
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hashstatic final int spread(int h) {return (h ^ (h >>> 16)) & HASH_BITS;
}

2.2. 初始化数组

无论是使用有参构造函数还是无参构造函数,最终都会调用 initTable() 方法来初始化哈希表(table

if (tab == null || (n = tab.length) == 0)tab = initTable();
private final Node<K,V>[] initTable() {Node<K,V>[] tab; int sc;//当前的初始化没有完成时,会一直进行 while循环while ((tab = table) == null || tab.length == 0) {//  如果sizeCtl < 0,则已经有线程进行初始化了或正在扩容,让当前线程等待一下if ((sc = sizeCtl) < 0)Thread.yield();// CAS 修改,尝试将 sizeCtl 从当前值 sc 更新为 -1else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {try {// 使用双重检查锁定来确保线程安全// 防止sizeCtl = sc刚赋值完,正好有线程走到这一步,不做限制的话就会重新初始化了if ((tab = table) == null || tab.length == 0) {// 无参初始化就是默认的 capacity,有参就是初始化时的 capacityint n = (sc > 0) ? sc : DEFAULT_CAPACITY;// 创建一个大小为 n 的数组,用于存储哈希桶。数组中的每个元素是一个链表或红黑树的头节点// 这里使用了类型转换,将通用的 Node<?,?>[] 转换为 Node<K,V>[],以适应泛型 K 和 VNode<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];// 将新创建的数组赋值给 table,表示哈希表已经初始化完成// 同时也将 tab 指向这个新数组,return。table = tab = nt;// 熟系的 0.75 负载因子 sc = n - (n >>> 2);}} finally {// 将扩容阈值赋值给 sizeCtlsizeCtl = sc;}break;}}return tab;
}

else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) 这里判断的是 SIZECTL,而不是size Ctl

private static final long SIZECTL = U.objectFieldOffset(ConcurrentHashMap.class.getDeclaredField("sizeCtl"));

SIZECTL 是 ConcurrentHashMap 类中一个静态常量,它代表 sizeCtl 变量在内存中的偏移量。在 compareAndSwapInt()操作中,SIZECTL 是用来告诉 JVM 如何找到 sizeCtl 变量的位置。

每个 Node<K,V> 对象表示 ConcurrentHashMap 中的一个键值对,并且可能是链表中的一个节点

static class Node<K,V> implements Map.Entry<K,V> {//哈希值,用于快速定位键在哈希表中的位置final int hash;// keyfinal K key;// valuevolatile V val;// 指向下一个节点,用于处理哈希冲突的链表结构volatile Node<K,V> next;
}

2.3. 将数据插入到数组中

// 如果当前数组该下标没有数据,直接插入
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {// CAS将当前的hash、key、value组装成一个Node,插入当前数组i位置if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null))){// 插入成功结束即可break;   }               
}// 返回当前数组下该下标的数据
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}// 更新数组
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,Node<K,V> c, Node<K,V> v) {return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
  • ASHIFT:表示数组中每个元素的大小(以字节为单位),通常是 4 或 8。
  • ABASE:表示数组的起始地址。通过这个基地址和偏移量,可以确定数组中某个元素的内存位置。

2.4. 协作扩容

暂时不讲扩容机制

// 如果当前位置正在扩容,就去协作扩容
else if ((fh = f.hash) == MOVED)tab = helpTransfer(tab, f);

2.5. 将数据插入到链表

V oldVal = null;
// 加锁对象是f,f只是对应一个下标的哈希槽,锁的范围很小
synchronized (f) {// 看一下当前数组的i位置是不是等于fif (tabAt(tab, i) == f) {// 如果 fh < 0,说明 f 是一个特殊节点(比如 TreeBin),此时进入红黑树处理逻辑if (fh >= 0) {// 记录当前哈希哈希槽下挂了多少个节点binCount = 1;// 从链表的第一个节点 f 开始,遍历整个链表,查找是否已经存在相同的键for (Node<K,V> e = f;; ++binCount) {K ek;// 如果当前节点的哈希值 e.hash 与要插入的键的哈希值相同// 且键 e.key 与要插入的键相同(通过 == 或 equals() 比较)if (e.hash == hash &&((ek = e.key) == key ||(ek != null && key.equals(ek)))) {// 将当前老的数据赋值给oldValoldVal = e.val;、// put() 出有设置// 如果为 false,key 一致时,直接覆盖数据// 如果为 true,key 一致时,什么都不做,key不存在,正常添加if (!onlyIfAbsent){e.val = value;}// 退出循环break;}Node<K,V> pred = e;// 如果遍历到链表的末尾 e.next == null// 说明链表中没有相同的键,创建一个新的节点并将其插入到链表的末尾。if ((e = e.next) == null) {pred.next = new Node<K,V>(hash, key, value, null);break;}}}// ...}
}

2.6. 将数据插入到红黑树

// 如果 f 是 TreeBin 的实例,说明哈希桶已经转换为红黑树结构
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;}}
}

2.7. 链表树化操作

// 树化阈值
static final int TREEIFY_THRESHOLD = 8;if (binCount != 0) {// 当前哈希槽下的节点数量超过树化阈值if (binCount >= TREEIFY_THRESHOLD)// 还需要判断数组的长度,并不是直接树化treeifyBin(tab, i);if (oldVal != null)return oldVal;break;
}
// 最小树化容量
static final int MIN_TREEIFY_CAPACITY = 64;private final void treeifyBin(Node<K,V>[] tab, int index) {Node<K,V> b; int n, sc;if (tab != null) {// 如果当前的数组长度小于64,没必要转成红黑树,直接扩容if ((n = tab.length) < MIN_TREEIFY_CAPACITY){// 扩容操作tryPresize(n << 1);}// 如果该哈希槽不为空,且节点是普通的链表节点,就进入树化操作else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {// 对哈希槽第一个节点加锁synchronized (b) {// 双重检查if (tabAt(tab, index) == b) {// hd(head):这是红黑树双向链表的头节点。// tl(tail):这是红黑树双向链表的尾节点TreeNode<K,V> hd = null, tl = null;// 遍历链表for (Node<K,V> e = b; e != null; e = e.next) {// 创建一个对应的红黑树节点 p,并将它的哈希值、键、值复制过来TreeNode<K,V> p = new TreeNode<K,V>(e.hash, e.key, e.val, null, null);// 构建双向链表if ((p.prev = tl) == null){hd = p;} else{tl.next = p;}tl = p;}// 完成链表到红黑树的转换后,并将红黑树其存储在哈希表的 index 处setTabAt(tab, index, new TreeBin<K,V>(hd));}}}}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/56646.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

流程图 LogicFlow

流程图 LogicFlow 官方文档&#xff1a;https://site.logic-flow.cn/tutorial/get-started <script setup> import { onMounted, ref } from vue import { forEach, map, has } from lodash-es import LogicFlow, { ElementState, LogicFlowUtil } from logicflow/core …

Jmeter监控服务器性能

目录 ServerAgent 安装 打开Jmeter ServerAgent 在Jmeter上监控服务器的性能比如CPU&#xff0c;内存等我们需要用到ServerAgent&#xff0c;这里可以下载我分享 ServerAgent-2.2.3.zip 链接: https://pan.baidu.com/s/1oZKsJGnrZx3iyt15DP1IYA?pwdedhs 提取码: edhs 安装…

FPGA图像处理之均值滤波

文章目录 一、什么是图像滤波&#xff1f;1.1 噪声类型1.2 滤波类型 二、均值滤波原理2.1 3*3窗口滑动过程2.2 图像扩展 三、Matlab实现均值滤波四、FPGA实现均值滤波4.1 生成 3*3 矩阵4.2 仿真3*3矩阵4.3 计算均值4.4 仿真均值滤波 一、什么是图像滤波&#xff1f; 图像滤波是…

得物App3D创新应用引关注,世界设计之都大会启幕

近日&#xff0c;2024世界设计之都大会&#xff08;WDCC&#xff09;在上海盛大启幕。此次大会以“设计无界 新质生长”为主题&#xff0c;汇聚了全球设计领域的精英与前沿成果&#xff0c;展现了设计作为新质生产力的巨大潜力。主场展览占据了整整3个楼面&#xff0c;总面积达…

C#学习笔记(十)

C#学习笔记&#xff08;十&#xff09; 第七章 对象的构造方法与实例方法一、对象的构造方法1. 构造方法初识2. 构造方法的创建3. this关键字4. 构造方法的规范和重载4.1 构造方法的规范 5. 对象初始化器5.1 对象初始化器和构造方法的区别 二、对象的实例方法1. 简单应用2.实例…

代码随想录算法训练营第二天(补) | 滑动窗口、模拟、前缀和

目录 3.4 长度最小的子数组 3.5螺旋矩阵II 3.6 区间和 文章讲解&#xff1a;[58. 区间和 | 代码随想录 3.4 长度最小的子数组 题目链接&#xff1a;. - 力扣&#xff08;LeetCode&#xff09; 文章讲解&#xff1a;代码随想录 视频讲解&#xff1a;拿下滑动窗口&#xff…

Linux历史

Linux 于 1991 年由芬兰学生 Linus Torvalds 作为个人项目开始&#xff0c;旨在创建一个新的免费操作系统内核。在其历史发展中&#xff0c;Linux 内核经历了持续的增长。自 1991 年首次发布源代码以来&#xff0c;Linux 内核从少量的 C 语言文件&#xff0c;且受限于禁止商业发…

机器视觉基础系列四—简单了解背景建模算法

机器视觉基础系列四—简单了解背景建模算法 首先我们应该了解的是背景建模的定义是什么&#xff1f;又有哪些应用场景呢&#xff1f; 背景建模是指通过分析视频序列中的像素值变化情况&#xff0c;从中提取出静态背景部分&#xff0c;并将其用于目标检测、运动跟踪等计算机视觉…

渗透测试导论

渗透测试的定义和目的 渗透测试&#xff08;Penetration Testing&#xff09;是一项安全演习&#xff0c;网络安全专家尝试查找和利用计算机系统中的漏洞。 模拟攻击的目的是识别攻击者可以利用的系统防御中的薄弱环节。 这就像银行雇用别人假装盗匪&#xff0c;让他们试图闯…

LeetCode1004.最大连续1的个数

题目链接&#xff1a;1004. 最大连续1的个数 III - 力扣&#xff08;LeetCode&#xff09; 1.常规解法&#xff08;会超时&#xff09; 遍历数组&#xff0c;当元素是1时个数加一&#xff0c;当元素是0时且已有的0的个数不超过题目限制时&#xff0c;个数加一&#xff0c;若上…

Lumerical学习——优化和参数扫描(Optimization and parameter sweeps)

一、概要介绍 这部分介绍优化和参数扫描项目设定的方法。在有大量数据模拟计算过程中这个特性允许用户使处理方法自动地查找期望的参数值。 ① 创建一个参数扫描任务 ② 创建一个优化任务 ③ 创建一个良率分析任务 ⑤ 打开所选择项目的编辑窗口&#xff0c;编辑其属性…

基于Java+SpringBoot+Uniapp的博客系统设计与实现

项目运行截图 技术框架 后端采用SpringBoot框架 Spring Boot 是一个用于快速开发基于 Spring 框架的应用程序的开源框架。它采用约定大于配置的理念&#xff0c;提供了一套默认的配置&#xff0c;让开发者可以更专注于业务逻辑而不是配置文件。Spring Boot 通过自动化配置和约…

嵌入式C++中内存分配基本实现方法

大家好,今天主要给大家分享一下,如何使用计算机中的内存空间进行分配,观察具体现象。 第一:C语言动态空间分配方式 第二:C++中动态内存分配方法 new 可以自动计算数据类型的大小 与 类型的转换 malloc 只能手动进行。 2.new 可以在分配空间的时候初始化 malloc 不行。 第三…

【优选算法】——双指针(上篇)!

&#x1f308;个人主页&#xff1a;秋风起&#xff0c;再归来~&#x1f525;系列专栏&#xff1a;C刷题算法总结&#x1f516;克心守己&#xff0c;律己则安 目录 前言&#xff1a;双指针 1. 移动零&#xff08;easy&#xff09; 2. 复写零&#xff08;easy&#xff09; 3…

计数型信号量

一&#xff0c;什么是计数型信号量&#xff1f; 计数型信号量相当于队列长度大于1 的队列&#xff0c;因此计数型信号量能够容纳多个资源&#xff0c;这在计数型信号量被创建的时候确定的。 计数型信号量相关 API 函数 函数描述xSemaphoreCreateCounting()使用动态方法创建计数…

工业相机详解及选型

工业相机相对于传统的民用相机而言&#xff0c;具有搞图像稳定性,传输能力和高抗干扰能力等&#xff0c;目前市面上的工业相机大多数是基于CCD&#xff08;Charge Coupled Device)或CMOS(Complementary Metal Oxide Semiconductor)芯片的相机。 一&#xff0c;工业相机的分类 …

Java 虚拟机实战(基础篇 1万字)

此笔记来自于黑马程序员 基础篇 初识 JVM(Java Virtual Machine) 什么是 JVM JVM 本质上是一个运行在计算机上的程序&#xff0c;他的职责是运行 Java 字节码文件 JVM 的功能 翻译成字节码 即时编译 Java语言如果不做任何优化&#xff0c;性能不如C、C等语言。Java 支持跨…

嬴图 | 图数据库系列 之 图算法与可解释性

2024年诺贝尔物理学奖颁发给了机器学习与神经网络领域的研究者&#xff0c;这是历史上首次出现这样的情况。这项奖项原本只授予对自然现象和物质的物理学研究作出重大贡献的科学家&#xff0c;如今却将全球范围内对机器学习和神经网络的研究和开发作为了一种能够深刻影响我们生…

手机怎么玩GTA5?GameViewer远程助你手机畅玩GTA5侠盗飞车

原来手机也可以玩电脑游戏&#xff01;如果你想随时随地用手机玩GTA5&#xff0c;网易GameViewer远程能帮你实现&#xff0c;它的按键映射功能会让你在体验GTA5时非常好。你不仅可以享受4K蓝光144帧高画质的 驾驶、第三人称射击&#xff0c;还有开放世界探索&#xff0c;还可以…

服务器软件之Tomcat

服务器软件之Tomcat 服务器软件之Tomcat 服务器软件之Tomcat一、什么是Tomcat二、安装Tomcat1、前提&#xff1a;2、下载3、解压下载的tomcat4、tomcat启动常见错误4.1、tomcat8.0 startup报错java.util.logging.ErrorManager: 44.2、java.lang.UnsatisfiedLinkError 三、Tomca…