ConcurrentHashMap核心源码(JDK1.8)

这篇具有很好参考价值的文章主要介绍了ConcurrentHashMap核心源码(JDK1.8)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一、ConcurrentHashMap的前置知识扫盲

ConcurrentHashMap的存储结构?

数组 + 链表 + 红黑树

ConcurrentHashMap核心源码(JDK1.8)

二、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中,采用的方式,可以实现为每一个索引位置都是一把独立的锁,不存在一个锁管理多个索引位置的情况,是一对一的方式。

ConcurrentHashMap核心源码(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的核心代码。

ConcurrentHashMap核心源码(JDK1.8)


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

size中的核心就是sumCount方法,在内部就是拿到baseCount,然后遍历CounterCell[],将内部的每一个value做 +=,最终计算出元素个数。文章来源地址https://www.toymoban.com/news/detail-471643.html

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;
}

到了这里,关于ConcurrentHashMap核心源码(JDK1.8)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • jdk1.7与jdk1.8的HashMap区别2-底层原理区别

    jdk1.7与jdk1.8的HashMap区别1-基本结构与属性对比_ycsdn10的博客-CSDN博客 1.代码数:JDK1.7与JDK1.8相差了一倍的代码 2.方法数:JDK1.7是40个,JDK1.8是51个(只算基本方法)  进行了4次位运算,5次亦或运算  进行了1次位运算,1次亦或运算 由上一章知道,JDK1.7是数组+链表,JDK1.8是数组

    2024年02月13日
    浏览(42)
  • jdk1.8安装

    1、卸载         #rpm -qa | grep java        查找已安装java         #rpm -qa | grep java | xargs rpm -e --nodeps        批量卸载 2、下载地址 Java Downloads | Oracle 3、上传到服务器,解压         #rz    jdk-8u371-linux-x64.tar.gz         #tar -zxvf jdk-8u241-linux-x64.tar.gz         #rpm ivh jdk-8u241-li

    2024年02月07日
    浏览(31)
  • SM4国密4在jdk1.7版本和jdk1.8版本中的工具类使用

    (一)首先,直接可用的工具类如下: 1、JDK1.8版本,使用hutool工具类实现SM4对称加密,pom依赖如下: 工具类实现: 打印信息: 2、JDK1.7版本,pom依赖如下: 工具类实现: 打印信息: 注:如果JDK1.7使用hutool包实现SM4,降低hutool的版本,也是可以实现的,实测可用: 注意: 如

    2024年02月05日
    浏览(49)
  • Mac安装配置jdk——jdk1.8,jdk11,jdk17

    我们日常工作中可能会在多个项目工程中来回切换,每个项目依赖的jdk版本也可能高低不同,这样会出现jdk版本高低的不兼容,工程代码编译不过,无法本地运行等问题。 那么能不能在一台电脑上装多个版本的jdk呢?多个jdk版本是否可以灵活切换呢? 答案是 可以的! 接下来

    2024年04月28日
    浏览(52)
  • 聊聊JDK1.0到JDK20的那些事儿

    最近小组在开展读书角活动,我们小组选的是《深入理解JVM虚拟机》,相信这本书对于各位程序猿们都不陌生,我也是之前在学校准备面试期间大致读过一遍,emm时隔多日,对里面的知识也就模糊了。这次开始的时候从前面的JDK发展史和JVM虚拟机家族着手,之前都是粗略读过

    2024年02月13日
    浏览(50)
  • JDK11相比JDK1.8有哪些新特性

    Java 11 相比 Java 8 引入了许多新的语言特性和 API,下面是一些主要的特性: HTTP Client API:Java 11 中引入了一个全新的原生 HTTP 客户端 API,用于替代老旧的 HttpURLConnection API。 动态类文件常量:Java 11 引入了动态类文件常量,可以在不加载类的情况下,将常量加入到已有的类定义

    2024年02月16日
    浏览(39)
  • linux安装jdk1.8

    为防止操作权限不足,建议切换root用户,当然如果你对Linux命令熟悉,能够自主完成权限更新操作,可以不考虑此推荐。 环境:centos7.6 ssh连接工具:tabby(自从用了这个工具,我再也不用xshell了,这个工具自带文件上传,还有网页版) 压缩包自己百度下载或者直接在下面的

    2024年02月02日
    浏览(47)
  • Map复习(JDK1.8)

    首先考虑的问题任然是高并发的安全问题: HashMap是一个不安全的,多线程会导致数据不一致。 Hashtable是安全的,但是速度比较慢,只要是安全的就是一性能为代价换来的,加锁开锁需要时间,加锁是只能一个线程访问. ConcurrentHashMap类也是安全的,增加时使用了synchronized保证

    2024年02月07日
    浏览(27)
  • Java中jdk1.8和jdk17相互切换

    之前做Java项目时一直用的是jdk1.8,现在想下载另一个jdk版本17,并且在之后的使用中可以进行相互切换,我将jdk切换时所遇到的问题记录下来并分享出来供大家参考。 环境变量配置如下: 步骤1 步骤2 (注:@MAVEN_HOME%bin;是配置maven时的环境变量,如果没有安装maven就不用管)

    2024年02月03日
    浏览(63)
  • JDK1.8 安装教程(linux)

    通过命令java –version 如果有出现如下图提示表示有安装,则无需再安装 通过JDK官网https://www.oracle.com/上下载需要的JDK 版本,下载完成后上传到linux 系统上指定的文件夹下。(可以用宝塔、finalshell、filezilla等工具进行上传) 通过cd 命令进入文件夹,用tar –zxvf 对jdk-8u131-linu

    2024年02月11日
    浏览(43)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包