【Java并发】聊聊concurrentHashMap的put核心流程

结构介绍

1.8中concurrentHashMap采用数组+链表+红黑树的方式存储,并且采用CAS+SYN的方式。在1.7中主要采用的是数组+链表,segment分段锁+reentrantlock。本篇主要在1.8基础上介绍下.
在这里插入图片描述
那么,我们的主要重点是分析什么呢,其实主要就是put 的整体流程。数组如何初始化,添加到数组、链表、红黑树、以及对应的扩容机制。

    //添加数据
    public V put(K key, V value) {
        return putVal(key, value, false);
    }

散列函数

put方法实际上调用的是putVal()方法。

 final V putVal(K key, V value, boolean onlyIfAbsent) {
        // 1.判断为空 直接返回空指针
        if (key == null || value == null) throw new NullPointerException();// key和value不允许null
        int hash = spread(key.hashCode());//两次hash,减少hash冲突,可以均匀分布
 }

这里为什么要对 h >>> 16 ,然后在进行 异或运算 & 操作。

其实主要还是为了让hash高16位也参与到索引位置的计算中。减少hash冲突。

    static final int spread(int h) {
        return (h ^ (h >>> 16)) & HASH_BITS;
    }

我们假设h 位 :00011000 00000110 00111000 00001100

00011000 00000110 00111000 00001100  h
^
00000000 00000000 00011000 00000110  h >>> 16


00011000 00000110 00111000 00001100 
&
00000000 00000000 00000111 11111111  2048 - 1


ConcurrentHashMap是如何根据hash值,计算存储的位置?
(数组长度 - 1) &  (h ^ (h >>> 16))

00011000 00000110 00110000 00001100  key1-hash
00011000 00000110 00111000 00001100  key2-hash
&
00000000 00000000 00000111 11111111  2048 - 1

Node中的hash值除了可能是数据的hash值,也可能是负数。

// static final int MOVED     = -1; // 代表当前位置数据在扩容,并且数据已经迁移到了新数组
// static final int TREEBIN   = -2; // 代表当前索引位置下,是一个红黑树。   转红黑树,TreeBin有参构造
// static final int RESERVED  = -3; // 代表当前索引位置已经被占了,但是值还没放进去呢。  compute方法

初始化数组

 final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();// key和value不允许null
        int hash = spread(key.hashCode());//两次hash,减少hash冲突,可以均匀分布
        int binCount = 0;//i处结点标志,0: 未加入新结点, 2: TreeBin或链表结点数, 其它:链表结点数。主要用于每次加入结点后查看是否要由链表转为红黑树
        for (Node<K, V>[] tab = table; ; ) {//CAS经典写法,不成功无限重试
            Node<K, V> f;
            int n, i, fh;
            //检查是否初始化了,如果没有,则初始化
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
   }

调用构造方法的时候,其实并没有进行初始化数组。而是延迟初始化操作。通过CAS的方式,先判断table数组为空的话,进行初始化。

   /**
     * 这里需要先介绍一下一个属性,sizeCtl是标识数组初始化和扩容的标识信息。
       = -1 正在初始化  < -1 : 正在扩容 =0 : 代表没有初始   
       > 0:①当前数组没有初始化,这个值,就代表初始化的长度!  ②如果已经初始化了,就代表下次扩容的阈值!
     */
    private transient volatile int sizeCtl;//控制标识符
// 初始化操作数组
 private final Node<K, V>[] initTable() {
        Node<K, V>[] tab;
        int sc;
        // 先判断是否为空
        while ((tab = table) == null || tab.length == 0) {
            // 说明线程正在初始化或者正在扩容 线程让步
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // lost initialization race; just spin
                // cas的方式将sizeCtl 修改成-1 正在初始化状态
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                   // 再次判断数组是否初始化没有
                    if ((tab = table) == null || tab.length == 0) {
                        // 获取到数组初始化的长度 16
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        // 初始化数组 16个Node数组
                        Node<K, V>[] nt = (Node<K, V>[]) new Node<?, ?>[n];
                        // 赋值给table全局变量
                        table = tab = nt;
                        // 计算下次扩容的阈值 也就是*2操作 
                        sc = n - (n >>> 2);
                    }
                } finally {
                    // 将最终的阈值设置给sizeCtl
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

添加数据-数组

数据添加到数组上(没有hash冲突)


 final V putVal(K key, V value, boolean onlyIfAbsent) {
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            // 上面的逻辑就是初始化数组    
            // 通过 n-1 & hash 计算当前数据的索引位置 
            // f 其实就是对应位置的引用 如果这个位置为空,说明这个数组都没有添加过数据
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                // CAS的方式构建一个Node阶段,然后将hash值,key,value存储到Node中
                // 跳出
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            // 这里说明f对应的hash为move 说明当前位置数据已经迁移到新数组。
            else if ((fh = f.hash) == MOVED)
               // 帮助扩容完。
                tab = helpTransfer(tab, f);
	}

添加数据-链表

final V putVal(K key, V value, boolean onlyIfAbsent) {
    // 拿到binCount
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        // n: 数组长度。 i:索引位置。  f:i位置的数据。 fh:是f的hash值
        Node<K,V> f; int n, i, fh;
        // 到这,说明出现了hash冲突,i位置有数据,尝试往i位置下挂数据
           else {
                V oldVal = null;
                //上面的逻辑如果都没有走到,那么说明出现了hash冲突, 
                //先进行加syn 锁,注意这个锁的粒度是一个桶的粒度。
                synchronized (f) {
                     // 再次判断
                    if (tabAt(tab, i) == f) {
                        // fh
                        if (fh >= 0) {
                            // binCount 赋值为1 记录链表中Node的长度
                            binCount = 1;
                            //e 指向数组位置数据 
                            // 在遍历链表的过程中,会记录当前链表的个数
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                // 拿到当前hash值比对
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                     // 走到这里,说明桶中的有元素就冲突
                                    oldVal = e.val;
                                // 如果是put方法,进去覆盖值
                                // 如果是putIfAbsent,进去不if逻辑
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                //走到这里。说明一直遍历完毕,说明桶中没有元素冲突。
                                // 挂在链表的最后
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                // 封装成一个Node节点
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        // 否则就是红黑树套路 
                        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;
                            }
                        }
                    }
                }
                //binCount不为0 
                if (binCount != 0) {
                    // 如果>= 8 那么进行扩容操作。
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        return null;

触发扩容

    // 判断是否需要转红黑树 或者扩容 
    private final void treeifyBin(Node<K,V>[] tab, int index) {
       //N : 数组 sc:sizeCtl
        Node<K,V> b; int n, sc;
        if (tab != null) {
            // 数组长度小于64 不转红黑树  先扩容(将更多的数据落到数组上)
            //只有数组长度>= 64并且链表长度达到8 才转为红黑树
            if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
                tryPresize(n << 1);
              // 转红黑树操作
              // 将单向链表转换为TreeNode对象(双向链表),再通过TreeBin方法转为红黑树。
              // TreeBin中保留着双向链表以及红黑树!
            else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
                synchronized (b) {
                   //.....
                }
            }
        }
    }

在这里插入图片描述

流程图

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

业界首款PCIe 4.0/5.0多通道融合接口SSD技术解读

之前小编写过一篇文章劝大家不要碰PCIe 5.0 SSD&#xff0c;详细内容&#xff0c;可以再回顾下&#xff1a; 扩展阅读&#xff1a;当下最好不要入坑PCIe 5.0 SSD 如果想要进一步了解PCIe 6.0&#xff0c;欢迎点击阅读&#xff1a; 浅析PCIe 6.0功能更新与实现的挑战 PCIe 6.…

【强化学习的数学原理-赵世钰】课程笔记(五)蒙特卡洛方法

目录 一.内容概述 二.激励性实例&#xff08;Motivating examples&#xff09; 三.最简单的基于 MC 的 RL 算法&#xff1a;MC basic 1.将策略迭代转换为无模型迭代&#xff08;Convert policy iteration to be model-free&#xff09; 2.The MC Basic algorithm 3.例子 …

无人驾驶卡尔曼滤波

无人驾驶卡尔曼滤波&#xff08;行人检测&#xff09; x k a x k − 1 w k x_k ax_{k-1} w_k xk​axk−1​wk​ w k w_k wk​&#xff1a;过程噪声 状态估计 估计飞行器状态&#xff08;高度&#xff09; x k z k − v k x_k z_k - v_k xk​zk​−vk​ 卡尔曼滤波通…

vivado 导入工程、TCL创建工程命令、

导入外部项目 您可以使用导入在Vivado IDE外部创建的现有RTL级项目文件Synopsys Synplify。Vivado IDE检测项目中的源文件并自动添加文件到新项目。设置&#xff0c;如顶部模块、目标设备和VHDL库 分配是从现有项目导入的。 1.按照创建项目中的步骤进行操作。 2.在“项目类…

Linux学习(13)——系统安全及应用

一、账号安全基本措施 1、系统账号清理 将非登录用户的Shell设为/sbin/nologin,及将用户设置为无法登录 锁定长期不使用的账户 删除无用的账户 锁定账户密码 本质锁定 shell——/sbin/nologin却比较特殊&#xff0c;所谓“无法登陆”指的仅是这个用户无法使用bash或其他sh…

忆阻器芯片STELLAR权重更新算法(清华大学吴华强课题组)

参考文献&#xff08;清华大学吴华强课题组&#xff09; Zhang, Wenbin, et al. “Edge learning using a fully integrated neuro-inspired memristor chip.” Science 381.6663 (2023): 1205-1211. STELLAR更新算法原理 在权值更新阶段&#xff0c;只需根据输入、输出和误差…

Qt/QML编程学习之心得:一个蓝牙音乐播放器的实现(30)

蓝牙bluetooth作为一种短距离的通信方式应用也是越来越广,比如很多智能家居、蓝牙遥控器、蓝牙音箱、蓝牙耳机、蓝牙手表等,手机的蓝牙功能更是可以和各种设备进行互联,甚至可以连接到车机上去配合wifi提供投屏、音乐等。那么如何在中控IVI上使用Qt来实现一个蓝牙音乐播放器…

山东名岳轩印刷包装携专业包装袋盛装亮相2024济南生物发酵展

山东名岳轩印刷包装有限公司盛装亮相2024第12届国际生物发酵展&#xff0c;3月5-7日山东国际会展中心与您相约&#xff01; 展位号&#xff1a;1号馆F17 山东名岳轩印刷包装有限公司是一家拥有南北两个生产厂区&#xff0c;设计、制版、印刷&#xff0c;营销策划为一体的专业…

编译原理期末大题步骤——例题

一、预测分析方法步骤 提取左公因子&#xff0c;消除左递归判断文法是否为LL(1)文法若是&#xff0c;构造预测分析表&#xff1b;否则&#xff0c;不能进行分析。根据预测分析表对输入串进行分析 例子&#xff1a; 文法G[E]&#xff1a; …

C#:让不安全代码(unsafe)运行起来

C#:让不安全代码&#xff08;unsafe&#xff09;运行起来 当我们VS中编写unsafe代码时&#xff0c;显示不能运行 这时只需要按照这里的提示&#xff0c;修改我们的属性设置即可 这样就可以运行了

AQS应用之BlockingQueue详解

概要 AQS全称是 AbstractQueuedSynchronizer&#xff0c;中文译为抽象队列式同步器。BlockingQueue&#xff0c;是java.util.concurrent 包提供的用于解决并发生产者 - 消费者问题的最有用的类&#xff0c;它的特性是在任意时刻只有一个线程可以进行take或者put操作&#xff0…

近似点梯度法

最优化笔记——Proximal Gradient Method 最优化笔记&#xff0c;主要参考资料为《最优化&#xff1a;建模、算法与理论》 文章目录 最优化笔记——Proximal Gradient Method一、邻近算子&#xff08;1&#xff09;定义 二、近似点梯度法&#xff08;1&#xff09;迭代格式&…

4.4 媒资管理模块 - 分布式任务处理介绍、视频处理技术方案

媒资管理模块 - 视频处理 文章目录 媒资管理模块 - 视频处理一、视频转码1.1 视频转码介绍1.2 FFmpeg 基本使用1.2.1 下载安装配置1.2.2 转码测试 1.3 工具类1.3.1 VideoUtil1.3.2 Mp4VideoUtil1.3.3 测试工具类 二、分布式任务处理2.1 分布式任务调度2.2 XXL-JOB 配置执行器 中…

Java诊断利器Arthas

https://arthas.aliyun.com/doc/https://arthas.aliyun.com/doc/ 原理 利用java.lang.instrument(容器类) 做动态 Instrumentation(执行容器) 是 JDK5 的新特性。使用 Instrumentation&#xff0c;开发者可以构建一个独立于应用程序的代理程序&#xff08;Agent&#xff09;&…

three.js实现电子围栏效果(纹理贴图)

three.js实现电子围栏效果&#xff08;纹理贴图&#xff09; 实现步骤 围栏的坐标坐标转换为几何体顶点&#xff0c;uv顶点坐标加载贴图&#xff0c;移动 图例 代码 <template><div class"app"><div ref"canvesRef" class"canvas-…

自带恒压恒流环路的降压型单片车充专用芯片

一、基本概述 XL2009是一款高效降压型DC-DC转换器&#xff0c;固定180KHz开关频率&#xff0c;可以提供最高2.5A输出电流能力&#xff0c;具有低纹波&#xff0c;出色的线性调整率与负载调整率特点。XL2009内置固定频率振荡器与频率补偿电路&#xff0c;简化了电路设计。 PWM …

基于图像合成和注意力的深度神经网络从计算机断层扫描灌注图像中自动分割缺血性脑卒中病变

Automatic ischemic stroke lesion segmentation from computed tomography perfusion images by image synthesis and attention-based deep neural networks 基于图像合成和注意力的深度神经网络从计算机断层扫描灌注图像中自动分割缺血性脑卒中病变背景贡献实验Comparison o…

EMQX5设置客户端连接认证

文章目录 说明配置客户端连接认证配置1、访问控制-客户端认证-创建2、选择“密码”方式-下一步3、选择“内置数据库”-下一步4、账号类型选择“username”5、密码加密方式选择“plain”6、加盐方式“disable”-创建7、添加客户端连接的账密客户端连接验证 心得 说明 号外&…

旋变检测AD2s1205手册学习笔记

旋变故障检测故障表 信号丢失检测 检测原理&#xff1a;任一旋变输入(正弦或余弦)降至指定的LOS正弦/余弦阈值 以下时&#xff0c;器件会检测到信号丢失(LOS)。AD2S1205通过将 监视信号与固定最小值进行比较检测此点 丢失的效果表现&#xff1a;LOS由DOS和LOT引脚均闩锁为逻辑…

从文本(.txt)文件中读取数据时出现中文乱码

前言 当需要从记事本中读取数据时&#xff0c;发现读取的数据会出现中文乱码&#xff0c;我尝试了C和C读取文件&#xff0c;发现都是这样。 乱码原因 文本文件的保存默认使用UTF-8编码方式&#xff0c;而VS编译器的编码方式是GBK&#xff0c;所以不同的编码方式导致了乱码。…