【Java】ConcurrentHashMap1.8源码保姆级解析

news2024/9/28 3:21:08

目录

ConcurrentHashMap 1.8的优化

初始化流程

扩容流程

读取数据流程

计数器的实现


ConcurrentHashMap 1.8的优化

  1. 存储结构的优化

    数组+链表 -> 数组+链表+红黑树
  2. 写数据加锁的优化

  3. 扩容的优化(协助优化)

  4. 计数器的优化

    LongAddr -> Cell[] 分段和汇总

线程安全,但是复合操作时只保证弱一致性/最终一致性 散列算法 当需要向ConcurrentHashMap中写入数据时,会根据key的hashcode来确定当前数据要放在数组的哪一个索引位置上,那么ConcurrentHashMap是如何实现散列算法的,我们来看看源码

​当向map中放数据时,我们用的是put()方法,这个方法在ConcurrentHashMap的源码中实际上是调用了putVal()方法

final V putVal(K key, V value, boolean onlyIfAbsent) {
    // 不允许key或者value值为null(HashMap没有这个限制🚫)
    if (key == null || value == null) throw new NullPointerException();
    // 根据key的hashCode计算出一个哈希值,后面得出当前key-value要存储在哪个数组索引位置
    int hash = spread(key.hashCode());
    // 后面会用到的一个标识
    int binCount = 0;
    ……
}

方法中先对key和value值进行了是否为空的判断,ConcurrentHashMap是不允许key或者value值为null的,这也是和HashMap的一个区别,然后开始计算🧮key的hashCode,以获取后面得出当前key-value要存储在哪个数组索引位置,而在计算key的hash值时,调用了一个名为spread的方法

​简单翻译下方法上面👆的那几行绿字

将哈希值的高位传播(XOR)到低位,并将最高位强制为0。 因为表使用了2次方掩码,仅在当前掩码以上的位数不同的一组哈希值总是会发生碰撞。(已知的例子包括在小表中持有连续整数的Float键的集合)。因此,我们应用一个转换,将高位的影响向下分散。在速度、实用性和位传播的质量之间有一个权衡。因为许多常见的哈希集已经是合理分布的(所以不受益于传播),而且因为我们使用树来处理大集的碰撞,我们只是以最便宜的方式XOR一些移位的比特,以减少系统损失,以及纳入最高比特的影响,否则由于表的界限,永远不会被用于索引计算。

这个方法将key的hashcode的高低16位进行"^"(异或)运算,最终又与HASH_BITS(如下图)进行"&"(与)运算,此处巧妙的使用了一个转换,通过将key的hashcode的右移16位,将其哈希值的高位向下传播到低位,并通过与HASH_BITS即0x7fffffff(Integer的最大值,最高位是0,其余都是1)进行的与运算将最高位强制为0

​简单来讲就是之前用传统的方式计算hashcode时,由于数组长度没那么长,就导致只有低位参与计算,产生哈希冲突的概率较高,将高位与地位进行异或运算可以使得高位也参与到运算中,尽可能的减少哈希冲突,此外负数在哈希值中有特殊含义(后面会介绍到),因此采用了spread()方法中的方式避免生成负的哈希值

​在调用spread()方法计算完哈希值后,进入了一段for循环,循环中先声明了4个变量

  • n:数组长度

  • i:当前Node要存放的索引位置

  • f:当前数组i索引位置上的Node对象

  • fn:当前数组i索引位置上数据的哈希值

接着大致按上图分为了三块逻辑:

  1. 对是否进行过初始化的处理

  2. 对是否处于扩容期间的处理

  3. 对数组下挂的结构已经从链表变成了红黑树情况的处理

详细流程如下:

  • 判断数组是否进行过初始化(先看能不能直接放在数组上)

    • 没有

      • 调用initTable()方法进行初始化

    • 已初始化
      • 基于i = (n - 1) & hash)计算出当前Node需要存放在哪个索引位置
      • 通过tabAt()方法获取哈希表i位置上的数据
        • 如果该位置为空 -> 该位置没有数据
        • 基于CAS方法将数据放在i位置上 -> 成功放置后结束循环
  • 哈希表i位置不为空(无法直接放在数组上,产生了哈希冲突,继续下面的逻辑)

    • 判断当前位置是否在扩容(fh = f.hash) == MOVED

      • 如果等于-1,则说明数组正在进行扩容,会调用helpTransfer()方法进行协助扩容(这也就是文章第一部分提到的1.8的新特性之一:协助扩容)

  • 如果不等于-1,则说明未在扩容期间,而且此时该位置下挂的不是一个链表,而是一棵红黑树,接下来的逻辑(截图中省略的部分)就是将数据存放到红黑树中的相应位置(其中涉及到红黑树的旋转问题,比较复杂,后后面考虑单独出一篇来分析)

这个方法差不多介绍完了,再问一个☝️问题:知道为什么ConcurrentHashMap的数组长度需要是2^n?

​上图中可以看到,在计算当前Node具体存放的索引位置时,使用了n-1,n代表数组长度,即这个索引位置是通过数组长度-1与通过spread()方法返回的哈希值与运算计算出的,当数组长度为2^n时,可以最大程度的避免哈希冲突,因此有着个要求,就算给ConcurrentHashMap传入的大小不是2^n,ConcurrentHashMap也会计算出大于该数的最小2^n,赋值给n。

初始化流程

数组是懒加载的,第一次执行put()方法的时候才会进行初始化

​initTable()就是初始化方法,这里面有一个很重要的变量sizeCtl

​sizeCtl是数组在初始化和扩容操作时的一个控制变量,他的不同值代表不同含义:

  • >0:代表当前数组已经初始化完成,此时sizeCtl的值代表当前数组的扩容阈值,或者数组的初始化大小

  • 0:代表当前数组还没初始化

  • -1:代表当前数组正在初始化

  • <-1:低16位代表当前数组正在扩容的线程个数

    • 如果是1个线程,值就是-2

    • 如果是2个线程,值就是-3

/**
 * Initializes table, using the size recorded in sizeCtl.
 */
private final Node<K,V>[] initTable() {
    // 声明标识
    Node<K,V>[] tab; int sc;
    // 判断有没有初始化
    while ((tab = table) == null || tab.length == 0) {
        // 判断sizeCtl是否小于0,即是否已经有线程在进行初始化了
        if ((sc = sizeCtl) < 0)
            Thread.yield(); // lost initialization race; just spin
        // 如果sizeCtl等于0表示当前线程为第一个进入map的线程
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            // U.compareAndSwapInt(this, SIZECTL, sc, -1) 以CAS的方式sizeCtl的值为-1
            // 修改成功则开始初始化
            try {
                // DCL 再次判断当前数组是否已经初始化完成
                if ((tab = table) == null || tab.length == 0) {
                    // 开始初始化
                    // sizeCtl>0 就初始化sizeCtl长度的数组
                    // sizeCtl=0 就初始化默认长度的数组
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    // 开始初始化
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    // 将sc赋值为下一次扩容的阈值
                    // 如果n=16 n>>>2=4 16-4=12
                    // 其实就是在当前数组的基础上,增加当前数组长度的0.75
                    sc = n - (n >>> 2);
                }
            } finally {
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

在开始初始化前,会先判断下sizeCtl的值:

  • sizeCtl>0 就初始化sizeCtl长度的数组

  • sizeCtl=0 就初始化默认长度的数组

实际真正初始化的过程就是new Node<?,?>[n],然后赋值给成员变量,接着还会通过sc = n - (n >>> 2)计算下一次扩容的阈值,举个例子,如果n=16,n>>>2的值就是4,然后16-4就等于12,也就是说,下次再需要扩容,就会在原来的基础上增加12,其实也就是增加当前数组长度的0.75。

我们根据对上面源码的分析,梳理下初始化的大致流程:

基于一个while循环,先去判断数组初始化了没有,如果没有,会再次比较sizeCtl的情况,如果正在初始化,则会通过Thread.yield()让出CPU资源,如果没有初始化,则会通过CAS的方式修改sizeCtl的值为-1,修改过后还会再次判断数组是否被初始化了(DCL,以免在当前线程修改sizeCtl的值的时候,已经有别的线程对当前数组完成了初始化),如果当前数组仍然没有被初始化,才会真正开始初始化。

扩容流程

主要有3种方式会触发扩容

1. 执行treeifyBin()方法进行链表转红黑树前,会先尝试通过tryPresize()方法进行数组扩容

当链表长度>8时,会尝试将链表转为红黑树(文章第一部分提到的1.8的结构优化)

​由上面的方法可以看到,在真正进行链表 -> 红黑树前,会先判断数组长度是否小于MIN_TREEIFY_CAPACITY即64

如果数组长度<64,会先尝试扩容操作

2. 执行putAll()方法时,会尝试通过tryPreSize()方法对数组进行扩容

putAll()方法会传入一个新Map,原先的数组很可能会不够用,因此出发扩容

​向tryPresize()中传入需要添加的Map的size

​首先要确定扩容的大小

大于最大值就取最大值(MAXIMUM_CAPACITY),不是2^n就计算大于它且离他最近的2^n

​接着就进入了while循环,这段循环内大致有3块逻辑

  1. 类似初始化的操作,可以参考上一部分初始化的代码分析

  2. 越界处理

  3. 扩容操作

    1. 计算扩容标识戳(用于协助扩容)

    2. 通过CAS修改sizeCtl的值来表示当前数组正在扩容中

    3. transfer()实现扩容操作

3. 执行add()操作时,如果当前元素个数达到了扩容阈值,也会进行扩容

读取数据流程

ConcurrentHashMap的数据查询都是以get()方法为入口的,我们直接来看看这个get()方法

​按上述源码,我们总结下查询流程:

  • 先判断当前key对应的value是否在数组上(基于key计算hash值)

    • 该位置数据为🈳️,数据不存在

      • 返回null

    • 该位置数据不为🈳️,数据存在

      • 直接查询对应数组的位置上的数据

        • hash值一致 + key一致 -> 返回该value

        • hash值<0(特殊情况,会通过find()方法进行value的获取)

          • 数据被迁移走了,此时会走ForwardingNode中对于find()方法的实现,方法中会到新数组nextTable中查询是否存在该value(逻辑类似)

          • 节点位置被占

          • 红黑树

  • 其他情况:数据在链表上

    • hash值一致 + key一致 -> 返回该value

  • 上面的情况都不满足,返回null

计数器的实现

计数器是用来统计ConcurrentHashMap的元素个数的,通过addCount()方法实现 可以看到addCount()方法中有2个重要的对象:CounterCell[]数组和baseCount变量

​他们是记录📝数据的两个位置:

  • 并发量不高时,会通过baseCount进行追加

  • 并发量较高时,CAS试图操作baseCount失败后,会切换到CounterCell[]数组来计数,有多个CounterCell可供不同的线程进行选择

当我们需要获取ConcurrentHashMap的元素个数时,会调用size()方法

​而size()方法中调用了sumCount()方法

在sumCount()对baseCount还有数组CounterCell[]中的每一个记录进行累加,返回最终值

搞定( ̄∇ ̄)/🎉~~~~~

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

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

相关文章

力扣算法练习(二)

主要参考自力扣官网解法 1.最长回文子串(5) 给你一个字符串 s&#xff0c;找到 s 中最长的回文子串。 如果字符串的反序与原始字符串相同&#xff0c;则该字符串称为回文字符串。 示例 1&#xff1a; 输入&#xff1a;s "babad" 输出&#xff1a;"bab"…

【Java】JVM学习(七)

JVM调优 堆空间如何设置 在分代模型中&#xff0c;各分区的大小对GC的性能影响很大。如何将各分区调整到合适的大小&#xff0c;分析活跃数据的大小是很好的切入点。 活跃数据的大小&#xff1a;应用程序稳定运行时长期存活对象在堆中占用的空间大小&#xff0c;也就是Full …

Git安装及使用图文教程详解(附带安装文件)

Git安装及使用图文教程详解&#xff08;附带安装文件&#xff09; 原创&#xff1a;丶无殇  2023-06-26 文章目录 下载安装下载安装验证安装成功版本查看 基础指令Git常用指令【首次必须】设置签名用户、邮箱1.初始化本地仓库2.查看本地库状态3.创建文件4.添加文件至暂存区5…

【DCT变换】Python矩阵运算实现DCT变换

一、前言 DCT变换&#xff08;离散余弦变换&#xff09; 是数字图像处理过程中广泛采用的一种操作&#xff0c;用于将空域的图像转换为频域表示&#xff0c;从而能够更有效地进行压缩、滤波和特征提取等处理。它在许多应用领域中发挥着重要的作用&#xff0c;尤其在图像和视频…

感知机(Perceptron)的原理及实现

1.感知机&#xff08;Perceptron&#xff09;的原理及实现 声明&#xff1a;笔记来源于《白话机器学习的数学》 感知机是接受多个输入后将每个值与各自权重相乘&#xff0c;最后输出总和的模型。 单层感知机因过于简单&#xff0c;无法应用于实际问题&#xff0c;但它是神经网络…

一文让你搞定接口测试

一、什么是接口测试&#xff1f; 所谓接口&#xff0c;是指同一个系统中模块与模块间的数据传递接口、前后端交互、跨系统跨平台跨数据库的对接。而接口测试&#xff0c;则是通过接口的不同情况下的输入&#xff0c;去对比输出&#xff0c;看看是否满足接口规范所规定的功能、…

二叉树知识小结

思维导图&#xff1a; 一&#xff0c;树 树&#xff0c;这是一种对计算机里的某种数据结构的形象比喻。比如这种: 这种&#xff1a; 这种&#xff1a; 这几种都是树形结构。在百度百科中对树形结构的定义如下&#xff1a; 树形结构指的是数据元素之间存在着“一对多”的树形关系…

津津乐道设计模式 - 建造者模式详解(教你如何构造一个专属女友)

&#x1f337; 古之立大事者&#xff0c;不惟有超世之才&#xff0c;亦必有坚忍不拔之志 &#x1f390; 个人CSND主页——Micro麦可乐的博客 &#x1f425;《Docker实操教程》专栏以最新的Centos版本为基础进行Docker实操教程&#xff0c;入门到实战 &#x1f33a;《RabbitMQ》…

接口测试断言详解(Jmeter)

接口测试是目前最主流的自动化测试手段&#xff0c;它向服务器发送请求&#xff0c;接收和解析响应结果&#xff0c;通过验证响应报文是否满足需求规约来验证系统逻辑正确性。接口的响应类型通过Content-Type指定&#xff0c;常见的响应类型有&#xff1a; • text/html &…

Android Jetpack Compose之轻松添加分隔线:Divider组件

引言&#xff1a; 在构建用户界面时&#xff0c;有效地组织和分隔内容是至关重要的。这就是Android Jetpack Compose的Divider组件派上用场的地方。在这篇博客中&#xff0c;我们将详细了解Divider组件的功能和用法&#xff0c;并通过示例展示如何将其融入您的Compose UI。 Je…

自动化测试和性能测试面试题精选

自动化测试相关 包含 Selenium、Appium 和接口测试。 1. 自动化代码中&#xff0c;用到了哪些设计模式&#xff1f; 单例模式工厂模式PO模式数据驱动模式 2. 什么是断言&#xff1f; 检查一个条件&#xff0c;如果它为真&#xff0c;就不做任何事&#xff0c;用例通过。如果…

8年资深测试总结,自动化测试成功实施,你不知道的都在这...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 什么项目&#xf…

python:并发编程(二十七)

前言 本文将和大家一起探讨python并发编程的实际项目&#xff1a;Locust性能测试&#xff08;篇一&#xff0c;共N篇&#xff09;&#xff0c;系列文章将会从零开始构建项目&#xff0c;并逐渐完善项目&#xff0c;最终将项目打造成适用于高并发场景的应用。 本文为python并发…

分支定价算法求解VRPTW问题(代码非原创)

参考文献&#xff1a;微信公众号“程序猿声”关于分支定价求解VRPTW的代码 A tutorial on column generation and branch-and-price for vehicle routing problems 框架 对于VRPTW问题&#xff0c;先做线性松弛&#xff0c;调用列生成算法&#xff08;一种解决大型线性规划问…

Docker网络之Network Namespace

Docker网络中相关的命令非常少&#xff0c;但需要掌握的底层原理却又非常多。 1.Network Namespace Docker网络底层原理是Linux的Network Namespace&#xff0c;所以说对于Linux Network Namespace的理解对Docker网络底层原理的理解就显得尤为重要了。 2.需求 通过手工的方式…

ICC2与INNOVUS命令对照表

ICC2与INNOVUS命令对照表 TargetICC2INNOVUS设置多CPU set_host_options -max_cores16 setMultiCpuUsage -localCpu 16 获得物体的属性 get_attribute

DSP,国产C2000横空出世,QX320F280049,替代TI 的 TMS320F280049,支持国产

一、特性参数 1、独立双核&#xff0c;32位CPU&#xff0c;单核主频400MHz 2、IEEE 754 单精度浮点单元 &#xff08;FPU&#xff09; 3、三角函数单元 &#xff08;TMU&#xff09; 4、1MB 的 FLASH &#xff08;ECC保护&#xff09; 5、1MB 的 SRAM &#xff08;ECC保护&…

全网最全,Selenium自动化测试POM模式总结(详细)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 在UI自动化测试中…

Python+Selenium+Unittest 之selenium7--元素定位6-CSS定位1(定位所有、定位class、定位id、tag定位)

目录 一、CSS简介 二、 定位方式 三、实践操作 1、*&#xff08;定位所有元素&#xff09; 2、. &#xff08;定位class属性&#xff09; 3、#&#xff08;定位id属性&#xff09; 4、tag定位 一、CSS简介 CSS属于是一种计算机语言&#xff0c;主要是用来为结构化文档的外…

软件测试期末速成(背题家出列!)

文章目录 一、前言二、选择题&#xff08;15 X 2&#xff09;1、概述2、相关概念3、黑盒测试4、白盒测试5、单元测试6、集成测试7、系统测试8、自动化测试9、实用软件测试技术 三、判断题&#xff08;10 X 1’&#xff09;四、简答题&#xff08;4 X 5&#xff09;1、软件测试生…