ConcurrentHashMap1.7 源码浅析
分析过HashMap的1.7的版本的结构,但是HashMap是线程不安全的,多线程触发扩容还会发生死循环问题,那么ConcurrentHashMap 就是解决这个问题的,这是一个线程安全的Map,那么对应的内部实现是怎么样的,简单分析下,和HashMap相同的位置就不多做重复分析了
构造方法
这是个最基础的构造方法,需要的参数有容量,扩容因子,这是和HashMap相同的地方,但是多了一个并发水平选项,这里默认值是16,也就是并发粒度的控制,最多可以16个线程同时加锁对Map处理,我们看下具体怎么做的。
public ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel) {if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)throw new IllegalArgumentException();if (concurrencyLevel > MAX_SEGMENTS)concurrencyLevel = MAX_SEGMENTS;// Find power-of-two sizes best matching argumentsint sshift = 0;int ssize = 1;// 小于并发等级就左移动一位,*2倍在得到一个大于等于这个数的2的n次幂while (ssize < concurrencyLevel) {++sshift;ssize <<= 1;}// sshift 记录的是对应的多少位// ssize记录的就是处理过的并发水平this.segmentShift = 32 - sshift;this.segmentMask = ssize - 1;if (initialCapacity > MAXIMUM_CAPACITY)initialCapacity = MAXIMUM_CAPACITY;// 这里在计算分段之后,每段的长度int c = initialCapacity / ssize;// 这里向上取下,因为上面int处理的直接向下取整了if (c * ssize < initialCapacity)++c;// 最小的分段长度为2int cap = MIN_SEGMENT_TABLE_CAPACITY;// 如果小于需要的值,就左移动一位,扩大二倍,知道大于等于需要的while (cap < c)cap <<= 1;// create segments and segments[0]// 对应的段,段的数组长度就是cap,长度的HashEntrySegment<K,V> s0 =new Segment<K,V>(loadFactor, (int)(cap * loadFactor),(HashEntry<K,V>[])new HashEntry[cap]);// 然后创建段的数组,长度就是之前处理过的并发粒度,可以保证锁段的时候并发粒度大于等于需要的Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];// cas 赋值UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]// 赋值给Map的成员变量this.segments = ss;}
从构造方法来看,就是对HashMap进行分段了,控制实际的容量大于等于需要的容量,并发粒度也是大于等于需要的粒度,这样可以对每一个段进行加锁,保证并发安全,又能保证一定的并发粒度,后面看下是怎么进行插入和获取的,是怎么进行加锁的,怎么保证数据安全的?
Segment 的结构
看下主要的结构,继承了ReentrantLock,同样维护了Map的扩容因子,扩容阈值,元素数量,Entry数组这些
static final class Segment<K,V> extends ReentrantLock implements Serializable {// 在预扫描之前尝试锁定的最大次数static final int MAX_SCAN_RETRIES =Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;// 对应的Entry数组,volatile修饰,修改数据对其他线程可见,及时刷新回主存// transient修饰,序列化忽略这个transient volatile HashEntry<K,V>[] table;// 分段内的元素的数量transient int count;// 修改相关的计数transient int modCount;// 扩容阈值transient int threshold;// 扩容因子final float loadFactor;}
put 方法分析
老规矩先从主要方法put开始看,put里面一般能看到存储结构,查找顺序等关键信息
public V put(K key, V value) {Segment<K,V> s;if (value == null)throw new NullPointerException();int hash = hash(key);// 计算位于哪个segmentint j = (hash >>> segmentShift) & segmentMask;// segment未初始化化的时候,进行初始化if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegments = ensureSegment(j);// 元素插入return s.put(key, hash, value, false);
}final V put(K key, int hash, V value, boolean onlyIfAbsent) {// 尝试获取锁,获取到了,第一步获取不到就是后面还是在获取,// 存在尝试次数,尝试次数完了之后死等,如果直接得到锁,返回的node就是null// 如果开始没得到锁,并且开始数组没数据的时候,会得到一个nodeHashEntry<K,V> node = tryLock() ? null :scanAndLockForPut(key, hash, value);// 上一步走完就是获取到锁了V oldValue;try {HashEntry<K,V>[] tab = table;int index = (tab.length - 1) & hash;// 获取对应下标的值,valitale保证及时可见HashEntry<K,V> first = entryAt(tab, index);for (HashEntry<K,V> e = first;;) {// 数组上有值if (e != null) {K k;// 遇见相同keyif ((k = e.key) == key ||(e.hash == hash && key.equals(k))) {//记录旧的值oldValue = e.value;if (!onlyIfAbsent) {e.value = value;++modCount;}break;}// 遍历下一个e = e.next;}else {// 数组上对应位置为空if (node != null)// 头插法,上面获取到了nodenode.setNext(first);elsenode = new HashEntry<K,V>(hash, key, value, first);int c = count + 1;if (c > threshold && tab.length < MAXIMUM_CAPACITY)// 大于扩容因子进行扩容,单独分析rehash(node);else// 否则直接添加setEntryAt(tab, index, node);++modCount;count = c;oldValue = null;break;}}} finally {// 解锁unlock();}return oldValue;
}
put方法就是先检查对应的segment是不是初始化了,未初始化的先进性初始化,初始化的时候按照第一个进行复制,然后cas赋值到对应位置,然后执行实际的插入逻辑,插入的时候先进行获取lock,因为Segnment继承的ReentrantLock,直接使用tryLock(),和lock()进行获取的非公平锁,获取之后对对应的值进行添加
ensureSegment 初始化segment分析
private Segment<K,V> ensureSegment(int k) {final Segment<K,V>[] ss = this.segments;long u = (k << SSHIFT) + SBASE; // raw offsetSegment<K,V> seg;if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {// 用构造方法时创建的第一个segment作为原型,复制一个segment出来Segment<K,V> proto = ss[0]; // use segment 0 as prototypeint cap = proto.table.length;float lf = proto.loadFactor;int threshold = (int)(cap * lf);// 对应的数组HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))== null) { // recheckSegment<K,V> s = new Segment<K,V>(lf, threshold, tab);// cas赋值segmentwhile ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))== null) {if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))break;}}}return seg;
}
scanAndLockForPut� 这里也是在获取锁,能抽空的话就会返回一个HashEntry
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {// 寻找hash对应的entryHashEntry<K,V> first = entryForHash(this, hash);HashEntry<K,V> e = first;HashEntry<K,V> node = null;int retries = -1; // negative while locating node// 获取不到锁,就一直获取while (!tryLock()) {HashEntry<K,V> f; // to recheck first below// 尝试次数if (retries < 0) {// 这里的e是遍历的当前节点if (e == null) {if (node == null) // speculatively create node// 这里的创建Entry应该就是简单的没别的事情做?充分利用下node = new HashEntry<K,V>(hash, key, value, null);retries = 0;}// 直接遇到相同的了else if (key.equals(e.key))retries = 0;// 遍历下一个elsee = e.next;}// 超过获取锁的最大尝试次数了else if (++retries > MAX_SCAN_RETRIES) {lock();break;}// 这里在判断尝试次数是不是为0,为0的上面两种,一种是对应数组位置无值,一种是有相同// key,可以直接替换的,//然后 后面的&& 判断了下是不是自己,如果不是自己插入的,更换下头节点,可能是别的线程// 插入的else if ((retries & 1) == 0 &&(f = entryForHash(this, hash)) != first) {e = first = f; // re-traverse if entry changedretries = -1;}}return node;
}
static final <K,V> HashEntry<K,V> entryForHash(Segment<K,V> seg, int h) {HashEntry<K,V>[] tab;// 判断段是不是空,里面数组是不是空,不空的时候获取对应的下标return (seg == null || (tab = seg.table) == null) ? null :(HashEntry<K,V>) UNSAFE.getObjectVolatile(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
}
�
rehash(node) 扩容方法分析
private void rehash(HashEntry<K,V> node) {HashEntry<K,V>[] oldTable = table;// 原来的容量int oldCapacity = oldTable.length;// 新的容量,扩大为2倍int newCapacity = oldCapacity << 1;threshold = (int)(newCapacity * loadFactor);//新的数组HashEntry<K,V>[] newTable =(HashEntry<K,V>[]) new HashEntry[newCapacity];int sizeMask = newCapacity - 1;// 遍历旧的for (int i = 0; i < oldCapacity ; i++) {HashEntry<K,V> e = oldTable[i];if (e != null) {HashEntry<K,V> next = e.next;int idx = e.hash & sizeMask;if (next == null) // Single node on list// 只有一个newTable[idx] = e;else { // Reuse consecutive sequence at same slot// 记录的当前遍历的头节点eHashEntry<K,V> lastRun = e;int lastIdx = idx;// 这里在遍历链表,last就是当前遍历到的for (HashEntry<K,V> last = next;last != null;last = last.next) {// 计算下标int k = last.hash & sizeMask;// 这里计算的是新下标的数据if (k != lastIdx) {// 不在现在这个位置的话,lastIdx = k;// 这个在记录链表最后一个不在本位置的节点// 获取这个引用的意义就在于,后面的不需要转移,直接就一串带走lastRun = last;}}// 然后赋值给新的位置newTable[lastIdx] = lastRun;// Clone remaining nodes// 然后重新遍历了一遍,遇到之前lastRun节点停止for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {V v = p.value;int h = p.hash;int k = h & sizeMask;// 采用头插法进行遍历插入到对应的链表中HashEntry<K,V> n = newTable[k];newTable[k] = new HashEntry<K,V>(h, p.key, v, n);}}}}// 扩容之后把node插入进去int nodeIndex = node.hash & sizeMask; // add the new nodenode.setNext(newTable[nodeIndex]);newTable[nodeIndex] = node;table = newTable;
}
可以看到resize基本等同HashMap,不过在resize里面把node进行插入的
get 方法
可以看到get方法比较简单,不需要加锁,通过volitale修饰的key,然后UNSAFE.getObjectVolatile 拿到对应的值
public V get(Object key) {Segment<K,V> s; // manually integrate access methods to reduce overheadHashEntry<K,V>[] tab;int h = hash(key);long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&(tab = s.table) != null) {for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);e != null; e = e.next) {K k;if ((k = e.key) == key || (e.hash == h && key.equals(k)))return e.value;}}return null;}
总结
整体总结下,ConcurrentHashMap 通过引入Segment概念来对HashMap进行分段,等于有了很多个小的HashMap,然后又继承了ReentrantLock,通过lock来保证put的数据安全性,并发粒度通过设置segment的数组长度来控制,默认16,可以自定义,不过也是2的n次幂,类似于原来hashmap的数组大小取值算法,对于segment里面,数组的最小长度为2,这个数组长度和segment的长度决定了容量大小,这里会大于等于设置的值,segment数组是不可变的,也就是map构造完成的时候,并发粒度就确定了,segment的长度大小不可变,扩容是在扩容的内部的HashMap,也就是HashEntry数组,整体就是hash计算了两次,第一次确认在那个segment,然后再计算落到那个段里面的哪个位置。
相关文章:
ConcurrentHashMap1.7 源码浅析
分析过HashMap的1.7的版本的结构,但是HashMap是线程不安全的,多线程触发扩容还会发生死循环问题,那么ConcurrentHashMap 就是解决这个问题的,这是一个线程安全的Map,那么对应的内部实现是怎么样的,简单分析…...
跨境电商时代的安全护航
随着跨境电商业务的蓬勃发展,网络安全问题日益突出。为了保障个人信息的安全和商业竞争的公平性,防关联浏览器和多开浏览器的需求日益增长。本文将为您介绍隐擎fox指纹浏览器,探讨其在跨境电商时代的重要作用,以及如何通过该浏览器…...
JavaScript Es6 _1 笔记
JavaScript Es6 _1 笔记 学习作用域、变量提升、闭包等语言特征,加深对 JavaScript 的理解,掌握变量赋值、函数声明的简洁语法,降低代码的冗余度。 理解作用域对程序执行的影响能够分析程序执行的作用域范围理解闭包本质,利用闭包…...

结构体和 Json 相互转换(序列化反序列化)
关于 JSON 数据 JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。易于人阅读和编写。同时也 易于机器解析和生成。RESTfull Api 接口中返回的数据都是 json 数据。 Json 的基本格式如下: { "a": "Hello", "b": "…...

【力扣刷题 | 第二十四天】
目录 前言: 416. 分割等和子集 - 力扣(LeetCode) 总结 前言: 今晚我们爆刷动态规划类型的题目。 416. 分割等和子集 - 力扣(LeetCode) 给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这…...
PyTorch使用(一)(常用库)
1.各大模型库 hub:简单来说就是专门为PyTorch集成的算法模型库 网站:GitHub - pytorch/hub: Submission to https://pytorch.org/hub/ Model Zoo:这个平台上提供预训练模型,在每个模型上,会标注出这个模型在GitHub的标…...
React ~ React Router 6
React Router 6 VS React Router 5.x 内置组件的变化; 移除<Switch /> , 新增<Routes />语法的变化; component { About } 变为 element { <About /> }新增多个hook官方明确推荐函数式组件了! 一级路由(变化) 安装路由 npm i react-router-dom (默认是最…...

【LeetCode每日一题】——304.二维区域和检索-矩阵不可变
文章目录 一【题目类别】二【题目难度】三【题目编号】四【题目描述】五【题目示例】六【题目提示】七【解题思路】八【时间频度】九【代码实现】十【提交结果】 一【题目类别】 矩阵 二【题目难度】 中等 三【题目编号】 304.二维区域和检索-矩阵不可变 四【题目描述】 …...

硬件串口通信协议学习(UART、IIC、SPI、CAN)
0.前言 学习资料:江协科技的个人空间-江协科技个人主页-哔哩哔哩视频 通信的目的:将一个设备的数据传送到另一个设备,扩展硬件系统通信协议:制定通信的规则,通信双方按照协议规则进行数据收发 全双工:通信…...

第一章-JavaScript基础进阶part2:事件
文章目录 概念一、注册事件(绑定事件)1.1 addEventListener事件监听 二、删除事件(解绑)三、DOM事件流四、事件对象event4.1 e.target与this与e.currentTarget的区别4.2 事件对象的常见属性 五、阻止事件默认行为及冒泡六、事件委…...
如何优雅的使用后端接口
优雅的后端接口 一个后端接口大致分为四个部分:接口地址(url)、接口请求方式(get、post等)、请求数据(request)、响 应数据(response)。 一、URL & Method Rest 设计风格 》 Restful API 简单理解: URI 是用来唯一标志一个互联网资源;Me…...
QEMU源码全解析25 —— QOM介绍(14)
接前一篇文章:QEMU源码全解析24 —— QOM介绍(13) 本文内容参考: 《趣谈Linux操作系统》 —— 刘超,极客时间 《QEMU/KVM》源码解析与应用 —— 李强,机械工业出版社 特此致谢! 本文开始对于…...
TopK问题
topK问题: N个数找最大或者最小的前k个。 例子: 优质筛选(店面的排名) 10000个数,找出最大的前10个数 解决思路:建立大堆,然后pop9次 但是有些场景,上面的思路…...

接口自动化测试-Postman+Newman+Git+Jenkins实战集成(详细)
目录:导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结(尾部小惊喜) 前言 1、Postman 创建…...
CMake 学习笔记 (Generator Expressions)
CMake 学习笔记 (Generator Expressions) Generator Expressions 可以认为是一种特殊的变量,它会在编译阶段求值。通常用在 target_link_libraries(), target_include_directories(), target_compile_definitions() 上。 用 Generator Expr…...

提高测试用例质量的6大注意事项
在软件测试中,经常会遇到测试用例设计不完整,用例没有完全覆盖需求等问题,这样往往容易造成测试工作效率低下,不能及时发现项目问题,无形中增加了项目风险。 因此提高测试用例质量,就显得尤为重要。一般来说…...

2023牛客暑期多校训练营6 A-Tree (kruskal重构树))
文章目录 题目大意题解参考代码 题目大意 ( 0 ≤ a i ≤ 1 ) , ( 1 ≤ c o s t i ≤ 1 0 9 ) (0\leq a_i\leq 1),(1 \leq cost_i\leq 10^9) (0≤ai≤1),(1≤costi≤109) 题解 提供一种新的算法,kruskal重构树。 该算法重新构树,按边权排序每一条边…...

软件测试—支付功能测试
有人问过我这样一个问题:作为一个支付平台,接入了快钱、易宝或直连银行等多家的渠道,内在的产品流程是自己的。业内有什么比较好的测试办法,来测试各渠道及其支持的银行通道呢? 回答:对支付平台而言&#…...

自动化测试的统筹规划
背景 回顾以前自动化测试编写的经历,主要是以开发者自驱动的方式进行,测试的编写随心而动,没有规划,也没有章法,这样就面临如下的一些问题: 测试用例设计不到位,覆盖不全,或者不够…...
外键字段的增删改查、多表查询(子查询和连表查询、正反向、聚合查询、 分组查询、 F与Q查询)、django中如何开启事务
一、 外键字段的增删改查 1.多对多的外键增删改查图书和作者是多对多,借助于第三张表实现的,如果想绑定图书和作者的关系,本质上就是在操作第三方表2.如何操作第三张表问题:让你给图书添加一个作者,他俩的关系可是多对…...
解锁数据库简洁之道:FastAPI与SQLModel实战指南
在构建现代Web应用程序时,与数据库的交互无疑是核心环节。虽然传统的数据库操作方式(如直接编写SQL语句与psycopg2交互)赋予了我们精细的控制权,但在面对日益复杂的业务逻辑和快速迭代的需求时,这种方式的开发效率和可…...
return this;返回的是谁
一个审批系统的示例来演示责任链模式的实现。假设公司需要处理不同金额的采购申请,不同级别的经理有不同的审批权限: // 抽象处理者:审批者 abstract class Approver {protected Approver successor; // 下一个处理者// 设置下一个处理者pub…...
Redis:现代应用开发的高效内存数据存储利器
一、Redis的起源与发展 Redis最初由意大利程序员Salvatore Sanfilippo在2009年开发,其初衷是为了满足他自己的一个项目需求,即需要一个高性能的键值存储系统来解决传统数据库在高并发场景下的性能瓶颈。随着项目的开源,Redis凭借其简单易用、…...
根目录0xa0属性对应的Ntfs!_SCB中的FileObject是什么时候被建立的----NTFS源代码分析--重要
根目录0xa0属性对应的Ntfs!_SCB中的FileObject是什么时候被建立的 第一部分: 0: kd> g Breakpoint 9 hit Ntfs!ReadIndexBuffer: f7173886 55 push ebp 0: kd> kc # 00 Ntfs!ReadIndexBuffer 01 Ntfs!FindFirstIndexEntry 02 Ntfs!NtfsUpda…...

数学建模-滑翔伞伞翼面积的设计,运动状态计算和优化 !
我们考虑滑翔伞的伞翼面积设计问题以及运动状态描述。滑翔伞的性能主要取决于伞翼面积、气动特性以及飞行员的重量。我们的目标是建立数学模型来描述滑翔伞的运动状态,并优化伞翼面积的设计。 一、问题分析 滑翔伞在飞行过程中受到重力、升力和阻力的作用。升力和阻力与伞翼面…...

【C++】纯虚函数类外可以写实现吗?
1. 答案 先说答案,可以。 2.代码测试 .h头文件 #include <iostream> #include <string>// 抽象基类 class AbstractBase { public:AbstractBase() default;virtual ~AbstractBase() default; // 默认析构函数public:virtual int PureVirtualFunct…...
【java】【服务器】线程上下文丢失 是指什么
目录 ■前言 ■正文开始 线程上下文的核心组成部分 为什么会出现上下文丢失? 直观示例说明 为什么上下文如此重要? 解决上下文丢失的关键 总结 ■如果我想在servlet中使用线程,代码应该如何实现 推荐方案:使用 ManagedE…...

解决MybatisPlus使用Druid1.2.11连接池查询PG数据库报Merge sql error的一种办法
目录 前言 一、问题重现 1、环境说明 2、重现步骤 3、错误信息 二、关于LATERAL 1、Lateral作用场景 2、在四至场景中使用 三、问题解决之道 1、源码追踪 2、关闭sql合并 3、改写处理SQL 四、总结 前言 在博客:【写在创作纪念日】基于SpringBoot和PostG…...

【SSM】SpringMVC学习笔记7:前后端数据传输协议和异常处理
这篇学习笔记是Spring系列笔记的第7篇,该笔记是笔者在学习黑马程序员SSM框架教程课程期间的笔记,供自己和他人参考。 Spring学习笔记目录 笔记1:【SSM】Spring基础: IoC配置学习笔记-CSDN博客 对应黑马课程P1~P20的内容。 笔记2…...
基于机器学习的智能故障预测系统:构建与优化
前言 在现代工业生产中,设备故障不仅会导致生产中断,还会带来巨大的经济损失。传统的故障检测方法依赖于人工巡检和定期维护,这种方式效率低下且难以提前预测潜在故障。随着工业物联网(IIoT)和机器学习技术的发展&…...