ConcurrentHashMap核心源码(JDK1.8)

news2024/12/25 0:33:45

一、ConcurrentHashMap的前置知识扫盲

ConcurrentHashMap的存储结构?

数组 + 链表 + 红黑树

二、ConcurrentHashMap的DCL操作

HashMap线程不安全,在并发情况下,或者多个线程同时操作时,肯定要使用ConcurrentHashMap

无论是HashMap还是ConcurrentHashMap,在new好之后,数组并不是直接初始化好的。

如果是这种懒汉式的初始化方式,ConcurrentHashMap需要保证初始化数组时,是线程安全的。

看源码之前,先掌握一个ConcurrentHashMap的核心属性,这个属性是控制扩容和初始化数组的核心属性。

private transient volatile int sizeCtl;

sizeCtl是控制数组的初始化和扩容的。

sizeCtl == -1: 代表数组正在初始化。

sizeCtl < -1: 代表数组正在扩容

sizeCtl == 0: ConcurrentHashMap刚刚new好,并且没指定数组的初始化长度(默认长度为16)

sizeCtl > 0:

  • ConcurrentHashMap刚刚new好,指定了数组的初始化长度,长度就是sizeCtl
  • ConcurrentHashMap已经在使用了,sizeCtl代表扩容的阈值(数组长度 * 0.75)

了解sizeCtl之后,开始看初始化数组的源码。

// ConcurrentHashMap初始化数组的方法
private final Node<K,V>[] initTable() {
    // 声明了两个属性,tab,sc
    Node<K,V>[] tab; int sc;
    // 判断数组初始化了咩? Check
    while ((tab = table) == null || tab.length == 0) {
        // 没初始化,准备初始化操作!
        // 给sc赋值,并且判断数组是否正在初始化。
        if ((sc = sizeCtl) < 0)
            // 让出CPU的时间片,等待其他更多的机会完成初始化数组操作。
            Thread.yield(); 
        // 没线程初始化,我来初始化。
        // 基于CAS的方式,将sizeCtl从原值改为-1,如果成功了,代表当前线程可以做初始化操作了。
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {  // Lock
            // 当前线程要开始初始化数组了。
            try {
                // 再次判断数组初始化了咩?  Check
                if ((tab = table) == null || tab.length == 0) {
                    // 数组没初始化。
                    // 获取数组的初始化长度。如果sizeCtl > 0 ,就用sizeCtl作为初始化的长度,否则使用默认的16
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    // 创建数组!
                    Node[] nt = new Node[n];
                    // 将初始化好的数组,赋值给成员变量
                    table = tab = nt;
                    // 算出下次扩容的阈值
                    sc = n - (n >>> 2);
                }
            } finally {
                // 将下次扩容的阈值赋值给sizeCtl,初始化完毕。
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

三、ConcurrentHashMap的散列算法

ConcurrentHashMap基于key的一系列运算,最种得出元素要放到数组的哪个索引位置上。

暂时就认为ConcurrentHashMap是将key调用了hashCode得到了一个int类型的数值。

其实计算索引位置就是将数组长度 - 1和key的hashCode值做&运算得出的结果就是索引位置。

// 先优化一下代码,看的更清楚
((f = tabAt(tab, i = (n - 1) & hash)) == null) 
// 代表拿到数组的某个索引位置的元素,f,如果为null准备插入数据
(f = table[(n - 1) & hash] == null)
// 这行就是在计算索引位置
(n - 1) & hash
n:数组的长度
hash:key.hashCode();
// n == 16
// hash:随便的一个int数值
00000000 00000000 00000000 00010000  :n        16
00000000 00000000 00000000 00001111  :n - 1    15
&
01010101 01010101 00110110 00101010  :hash
=
00000000 00000000 00000000 00001010  :index    10 
如果ConcurrentHashMap的数组长度,允许17的话,会出现什么情况
00000000 00000000 00000000 00010001  :n        17
00000000 00000000 00000000 00010000  :n - 1    16
&
01010101 01010101 00110110 00111010  :hash
=
00000000 00000000 00000000 000 0000  :index    10

散列算法

目的就是让key的HashCode值的高低16位进行亦或运算,再和数组长度 - 1做&运算,最终得到元素存储的位置。

00000000 00000000 00000000 00010000  :n        16 
00000000 00000000 00000000 00001111  :n - 1    15

01010101 01010101 00110110 00101010  :name.hashCode();
00000001 01000001 00000110 00101010  :age.hashCode();

散列算法就对这种情况做了优化!
散列算法:
(h ^ (h >>> 16)) & HASH_BITS;
优化一波
h ^ (h >>> 16)

01010101 01010101 00110110 00101010  :name.hashCode()
^
00000000 00000000 01010101 01010101  :name.hashCode() >>> 16
=
00000000 00000000 00000000 00001111  : 最终name的hashCode


00000001 01000001 00000110 00101010  :age.hashCode();
^
00000000 00000000 00000001 01000001  :age.hashCode() >>> 16
=
00000000 00000000 00000000 00001011  : 最终age的hashCode

为什么spread会让hash值和HASH_BITS做&运算

发现spread方法里,得到hash值之后,还做了一波&运算。
hash & HASH_BITS;
HASH_BITS = 01111111111111111111111111111111
hash值和HASH_BITS做&运算后,得到的结果除了最高位是0之外,其他位数没变化。
目的就是确保hash值算出来的一定是一个正数,因为负数有特殊含义。
static final int MOVED     = -1;  如果存在数组中的数据的hash为-1,代表当前数组正在扩容!
static final int TREEBIN   = -2;  如果存在数组中的数据的hash为-2,代表当前索引位置下挂的是红黑树!
static final int RESERVED  = -3;  如果存在数组中的数据的hash为-3,当前当前数组的索引位置已经被占用了(value还没计算出来)

四、ConcurrentHashMap的并发安全(写数据)

首先确认ConcurrentHashMap在并发执行写操作时,线程是安全的。

同时还需要保证效率要高。

在JDK1.7中的实现是采用Segement分段锁的形式实现的。

Segement锁的本质就是ReentrantLock,一个Segement会管理多个索引位置,当操作指定索引位置前,需要先去或者这个索引位置对应的锁,再来执行操作。 这种方式在数组长度变长之后,效率也就一般般。

在JDK1.8中,采用的方式,可以实现为每一个索引位置都是一把独立的锁,不存在一个锁管理多个索引位置的情况,是一对一的方式。

代码实现的效果。 WCWCWCWCWCWCWCWCWCWCWCWCWC!!!!!

for (Node<K,V>[] tab = table;;) {
    // 省略部分代码 
    else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
        // 进到这,说明当前数组的索引位置,没数据,数据要放到数组上
        // 当数据要放到数组上时,基于CAS的形式存放。
        if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
            break;          
    }
    else {
        // 进到这,说明数组的索引位置有数据,数据要挂到链表或者红黑树上
        V oldVal = null;
        // f就是索引位置上的Node对象
        // 当操作时,根据数组上的f进行加锁,实现锁的细粒度化~
        synchronized (f) {
            // 省略部分代码 
        }
    }
}

五、ConcurrentHashMap的计数器和size方法

计数器:每次ConcurrentHashMap在写入一个数据后,需要+1,删除一个数据后,需要-1

size方法:帮你返回当前ConcurrentHashMap中的元素个数。

计数器需要保证线程安全的同时,实现++操作,一般就采用CAS,Java中在JUC好下,恰巧提供了Atmoic的原子类,内部已经帮你实现的了,基于CAS的++操作。

发现AtmoicInteger这种提供了increment操作的原子类中,是基于do-while + CAS实现的,如果并发比较大的话,会造成不停的CAS,导致浪费CPU资源。

所以ConcurrentHashMap并没有使用AtmoicInteger的方式去实现++的线程安全,是采用了一个LongAdder的实现机制。LongAdder有一个类似分段锁的概念。

ConcurrentHashMap并没有直接调用LongAdder,而是再次实现了LongAdder的核心代码。


size方法,就是将BaseCount和CounterCell数据的值进行一波统计,最终得出结果。

size中的核心就是sumCount方法,在内部就是拿到baseCount,然后遍历CounterCell[],将内部的每一个value做 +=,最终计算出元素个数。

final long sumCount() {
    CounterCell[] as = counterCells; CounterCell a;
    long sum = baseCount;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    return sum;
}

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

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

相关文章

Ceph分布式存储 原理+架构图详解

存储基础 单机存储设备 ●DAS&#xff08;直接附加存储&#xff0c;是直接接到计算机的主板总线上去的存储&#xff09; IDE、SATA、SCSI、SAS、USB 接口的磁盘 所谓接口就是一种存储设备驱动下的磁盘设备&#xff0c;提供块级别的存储 ●NAS&#xff08;网络附加存储&#x…

2、MySQL数据库基础

目录 MySQL 连接查询 表 约束 存储引擎 事务 索引 视图&#xff08;View&#xff09; 数据库的导入导出&#xff08;DBA命令&#xff09; 数据库设计三范式 MySQL sql、DB、DBMS分别是什么&#xff1f;它们之间的关系&#xff1f; DB&#xff1a; DataBase&#xff0…

软考A计划-电子商务设计师-系统开发项目管理

点击跳转专栏>Unity3D特效百例点击跳转专栏>案例项目实战源码点击跳转专栏>游戏脚本-辅助自动化点击跳转专栏>Android控件全解手册点击跳转专栏>Scratch编程案例 &#x1f449;关于作者 专注于Android/Unity和各种游戏开发技巧&#xff0c;以及各种资源分享&am…

华为OD机试真题 Java 实现【最小传输时延】【2023 B卷 100分】,附详细解题思路

一、题目描述 某通信网络中有N个网络节点&#xff0c;用1到N进行标识。 网络通过一个有向无环图表示&#xff0c;其中图的边的值表示结点之间的消息传递时延。 现给定相连节点之间的时延列表times[i] {u,v,w}&#xff0c;u表示源节点&#xff0c;v表示目的节点&#xff0c;…

每日一博 - Server-Sent Events推送技术

文章目录 概述SSE VS WS一、实现方式二、应用场景三、性能方面四、小结结 Code在Spring Boot中使用SSE测试总结 概述 SSE&#xff08;Server-Sent Events&#xff09;是一种基于HTTP的服务器推送技术&#xff0c;它允许服务器实时地向客户端推送数据。相比于传统的轮询或长轮询…

计算机网络|第六章:链路层和局域网

目录 &#x1f4da;链路层概述 &#x1f407;链路层提供的服务 &#x1f407;链路层在何处实现 &#x1f4da;差错检测和纠正技术 &#x1f407;奇偶校验 &#x1f407;检验和方法 &#x1f407;循环冗余检测⭐️ &#x1f4da;多路访问链路和协议 &#x1f407;信道划…

前端:开源免费的浏览器端Markdown编辑器——Vditor上手体验

今天给大家聊聊一款开源免费的浏览器端Markdown编辑器——Vditor&#xff0c;非常的好用&#xff0c;分享给大家&#xff01; 一、编辑器简介 Vditor 是一款浏览器端的 Markdown 编辑器&#xff0c;支持所见即所得、即时渲染&#xff08;类似 Typora&#xff09;和分屏预览模式…

chatgpt赋能python:用Python轻松处理奇偶数——Python奇偶数处理教程

用Python轻松处理奇偶数——Python奇偶数处理教程 什么是奇偶数&#xff1f; 在数学中&#xff0c;任何整数都可以被分为两类&#xff1a;奇数和偶数。奇数是指不能被2整除的整数&#xff0c;而偶数是指可以被2整除的整数。例如&#xff0c;1、3、5、7等都是奇数&#xff0c;…

阵列卡缓存 RAID Cache

简介 磁盘阵列(Redundant Arrays of Independent Drives&#xff0c;RAID)&#xff0c;有“独立磁盘构成的具有冗余能力的阵列”之意。 RAID卡电路板上的一块存储芯片&#xff0c;与硬盘盘片相比&#xff0c;具有极快的存取速度&#xff0c;实际上就是相对低速的硬盘盘片与相…

TypeScript 的魔法技能:satisfies

现在&#xff0c;随着 TS 4.9 的发布&#xff0c;在 TypeScript 中有了一种新的、更好的方式来做类型安全校验。它就是 satisfies &#xff1a; type Route { path: string; children?: Routes } type Routes Record<string, Route>const routes {AUTH: {path: &quo…

MySQL-索引详解(上)

♥️作者&#xff1a;小刘在C站 ♥️个人主页&#xff1a;小刘主页 ♥️每天分享云计算网络运维课堂笔记&#xff0c;努力不一定有回报&#xff0c;但一定会有收获加油&#xff01;一起努力&#xff0c;共赴美好人生&#xff01; ♥️树高千尺&#xff0c;落叶归根人生不易&…

零入门kubernetes网络实战-34->将物理网卡eth0挂载到虚拟网桥上使得内部网络能够跨主机ping通外网的方案

《零入门kubernetes网络实战》视频专栏地址 https://www.ixigua.com/7193641905282875942 本篇文章视频地址(稍后上传) 本篇文章模拟一下啊&#xff0c;将宿主机的对外的物理网卡&#xff0c;挂载到虚拟网桥上&#xff0c;测试一下&#xff0c; 网桥管理的内部网络如何跟宿主…

华为OD机试真题 Java 实现【太阳能板最大面积】【2022Q4 100分】,附详细解题思路

一、题目描述 给航天器一侧加装长方形或正方形的太阳能板&#xff08;图中的红色斜线区域&#xff09;&#xff0c;需要先安装两个支柱&#xff08;图中的黑色竖条&#xff09;&#xff0c;再在支柱的中间部分固定太阳能板。 但航天器不同位置的支柱长度不同&#xff0c;太阳…

Linux账号管理与ACL权限设定(二)

使用者身份切换 通常以一般账号登录系统&#xff0c;若有系统维护或软件更新才需要转为root身份来操作。 su 若要完整的切换到新使用者的环境&#xff0c;必须要用 su – username &#xff0c;才会连同环境 PATH/USER/MAIL 等变量都转成新用户的环境。 若仅想执行一次root…

Linux学习[14]默认文本编辑vi/vim介绍常用指令演示指令汇总

文章目录 前言&#xff1a;1. vi介绍2. 指令演示2.1 vi创建文件2.2 添加文本 3. 指令汇总3.1 一般指令模式可用的按钮说明&#xff0c;光标移动、复制贴上、搜寻取代等3.2 进入插入或取代的编辑模式3.3 一般指令模式切换到命令行界面的可用按钮说明 总结 前言&#xff1a; 之前…

pycharm和virtualBox虚拟机的安装(包括本地环境和远程环境配置)

目录 一、安装时需要的软件二、安装virtualBox三、安装pycharm四、创建pycharm本地环境五、创建pycharm远程环境 一、安装时需要的软件 Pycharm&#xff0c;jetbrains-agent-latest破解包&#xff08;破解pycharm&#xff09;;镜像文件ubuntu20&#xff0c;虚拟机virtualBox …

【Android】通过Profiling工具和adb确定app被系统杀死的原因

当您想要确定安卓App被系统杀死的原因时&#xff0c;可以通过以下步骤进行分析&#xff1a; 打开Android Studio中的Profiling工具 在您的项目中&#xff0c;打开Android Studio并进入Profiling工具。点击左上角的“Android Profiler”图标&#xff0c;选择“CPU”或“Memor…

【Linux】Linux编译器 gcc/g++的使用初识动静态链接库

​ ​&#x1f4dd;个人主页&#xff1a;Sherry的成长之路 &#x1f3e0;学习社区&#xff1a;Sherry的成长之路&#xff08;个人社区&#xff09; &#x1f4d6;专栏链接&#xff1a;Linux &#x1f3af;长路漫漫浩浩&#xff0c;万事皆有期待 上一篇博客&#xff1a;【Linux】…

chatgpt赋能python:Python处理DICOM文件

Python处理DICOM文件 DICOM (Digital Imaging and Communications in Medicine)是医疗图像和数据的标准&#xff0c;用于存储和交换医学图像和相关信息。在医疗领域中&#xff0c;DICOM文件是医生和医学技师进行诊断和治疗的必要条件。在本文中&#xff0c;我们将介绍如何使用…

Linux下配置Qt6安卓开发环境

安装JDK 选择自己定义JDK安装路径 点击如下图按钮 安装SDK 提示TLS初始化失败 由于HTTPS问题造成无法下载,暂用Android Studio来安装Android SDK 成功安装SDK 安装NDK与命令行工具 正在下载NDK及命令行工具 NDK与工具下载完成 配置QT的Android SDK路径 配置NDK路径 选择ND…