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

news2024/11/13 15:22:28

结构介绍

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) {
                   //.....
                }
            }
        }
    }

在这里插入图片描述

流程图

在这里插入图片描述

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

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

相关文章

强化学习在生成式预训练语言模型中的研究现状简单调研

1. 绪论 本文旨在深入探讨强化学习在生成式预训练语言模型中的应用&#xff0c;特别是在对齐优化、提示词优化和经验记忆增强提示词等方面的具体实践。通过对现有研究的综述&#xff0c;我们将揭示强化学习在提高生成式语言模型性能和人类对话交互的关键作用。虽然这些应用展示…

kubernetes volume 数据存储详解

写在前面&#xff1a;如有问题&#xff0c;以你为准&#xff0c; 目前24年应届生&#xff0c;各位大佬轻喷&#xff0c;部分资料与图片来自网络 内容较长&#xff0c;页面右上角目录方便跳转 概述 容器的生命周期可能很短&#xff0c;会被频繁的创建和销毁 保存在容器中的…

Leetcode刷题笔记题解(C++):无重复字符的最长子串

思路&#xff1a; 利用滑动窗口的思想&#xff0c;用起始位置startindex和curlength来记录这个滑动窗口的大小&#xff0c;并且得出最长距离&#xff1b;利用哈希表来判断在滑动窗口中是否存在重复字符&#xff0c;代码如下所示&#xff1a; class Solution { public:int len…

6.1 截图工具HyperSnap6简介

图片是组成多媒体作品的基本元素之一&#xff0c;利用图片可以增强多媒体作品的亲和力和说说服力。截取图片最简单的方法是直接按下键盘上的“PrintScreen”键截取整个屏幕或按下“AltPrintScreen”组合键截取当前活动窗口&#xff0c;然后在画笔或者其它的图片处理软件中进行剪…

基于SSM的在线电影票购买系统(有报告)。Javaee项目。ssm项目。

演示视频&#xff1a; 基于SSM的在线电影票购买系统&#xff08;有报告&#xff09;。Javaee项目。ssm项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&#xff0c;通过Spring…

【模拟IC学习笔记】Cascode OTA 设计

辅助定理 增益Gm*输出阻抗 输出短路求Gm 输入置0求输出阻抗 求源极负反馈的增益 随着Vin的增加&#xff0c;Id也在增加&#xff0c;Rs上压降增加&#xff0c;所以&#xff0c;Vin的一部分电压体现在Rs上&#xff0c;而不是全部作为Vgs&#xff0c;因此导致Id变得平滑。 Rs足…

软件测试|MySQL DISTINCT关键字过滤重复数据

简介 在MySQL中&#xff0c;有时候我们需要从表中检索唯一的、不重复的数据。这时&#xff0c;我们可以使用DISTINCT关键字来过滤掉重复的数据行。在本文中&#xff0c;我们将深入探讨MySQL中DISTINCT的用法以及如何在查询中使用它来得到不重复的结果集。 基本语法 DISTINCT…

Influxdb2修改管理员密码

通过恢复管理员令牌来重置InfluxDB2管理员的密码 1.找到数据库的配置文件 一般为config.json 2.配置文件的的blod文件配置 3.在这个混合文本和二进制json文件中搜索已知的用户名或token之类的字符串。 例如&#xff1a; "id":"0bd73badf2941000","…

系列十四、理解MySQL varchar(50)

一、理解MySQL varchar(50) 1.1、概述 日常开发中&#xff0c;数据库建表是必不可少的一个环节&#xff0c;建表的时候通常会看到设定某个字段的长度为varchar(50)&#xff0c;例如如下建表语句&#xff1a; 那么怎么理解varchar(50)&#xff1f;这个分情况的&#xff0c;MySQ…

美创科技葛宏彬:夯实安全基础,对医疗数据风险“逐个击破”

导读 解决医疗机构“临床业务数据合规流动”与“重要数据安全防护”两大难题。 2023年11月11日&#xff0c;在2023年南湖HIT论坛上&#xff0c;HIT专家网联合杭州美创科技股份有限公司&#xff08;以下简称美创科技&#xff09;发布《医疗数据安全风险分析及防范实践》白皮书…

遇到U盘写保护怎么办

U盘写保护 为什么出现写保护的情况 U盘写保护&#xff0c;就是无法对U盘数据进行修改&#xff08;添加、删除、修改名称&#xff09;。 u盘写保护分为硬件写保护、系统或软件异常导致的写保护。 硬件写保护一般是U盘上硬件写保护开关被开启&#xff08;常见于SD卡读卡器侧面会…

【大数据架构】日志采集方案对比

整体架构 日志采集端 Flume Flume的设计宗旨是向Hadoop集群批量导入基于事件的海量数据。系统中最核心的角色是agent&#xff0c;Flume采集系统就是由一个个agent所连接起来形成。每一个agent相当于一个数据传递员&#xff0c;内部有三个组件&#xff1a; source: 采集源&…

HubSpot线索管理系统怎么样?适合哪些企业?

HubSpot的线索管理系统是该平台中销售和市场营销工具的一部分&#xff0c;被称为HubSpot CRM&#xff08;客户关系管理&#xff09;。以下是对HubSpot CRM的一些特点以及适合的企业类型的概述&#xff1a; HubSpot CRM 特点&#xff1a; 1. 用户友好的界面&#xff1a; HubS…

OpenAI ChatGPT-4开发笔记2024-02:Chat之text generation之completions

API而已 大模型封装在库里&#xff0c;库放在服务器上&#xff0c;服务器放在微软的云上。我们能做的&#xff0c;仅仅是通过API这个小小的缝隙&#xff0c;窥探ai的奥妙。从程序员的角度而言&#xff0c;水平的高低&#xff0c;就体现在对openai的这几个api的理解程度上。 申…

【hcie-cloud】【20】容器详解【容器介绍,容器工作机制、容器常用命令说明】【上】

文章目录 前言容器是什么虚拟化技术的四个特点容器也是一种虚拟化技术容器是怎么实现虚拟化的&#xff1f;容器对比虚拟机有哪些优势&#xff1f;容器对比虚拟机有哪些不足&#xff1f;容器不仅是一种虚拟化技术&#xff0c;更重要的是一种应用打包机制容器提供的是PaaS服务常见…

JavaSec基础 反射修改Final修饰的属性及绕过高版本反射限制

反射重拾 半年没碰java了 先写点基础回忆一下 反射弹计算器 public class Test {public static void main(String[] args) throws Exception {Class<?> clazz Class.forName("java.lang.Runtime");clazz.getDeclaredMethod("exec", String.cla…

Aging:浙大学者研究发现,多吃这类抗氧化饮食,延缓衰老

撰文 | 宋文法 衰老&#xff0c;是一个复杂、多阶段、渐进的过程&#xff0c;发生在生命的整个过程。随着时间的流逝&#xff0c;人体的器官、肌肉会逐渐衰老&#xff0c;一些疾病也伴随着年龄的增长而发生&#xff0c;包括癌症、糖尿病、心血管疾病等。 衰老过程是由体内自由基…

autoxjs 安卓爬虫自动化

autoxjs 安卓爬虫自动化 我这里只是测试请勿用于违法的 我这里是小红书 文章目录 autoxjs 安卓爬虫自动化前言一、自动刷直播间并且抓取商品已经粉丝数量等&#xff1f;总结 前言 欢迎来到AutoXJS的世界&#xff0c;这是一个充满创新、挑战和技术探索的领域。在这个引领未来的…

【huggingface】【pytorch-image-models】timm框架中使用albumentations库数据增广

文章目录 一、前言二、实操2.1 声明库2.2 定义你的数据增广算子2.3 加入其中 一、前言 问题是这样的&#xff0c;在使用timm框架训练时&#xff0c;发现数据增广不够&#xff0c;想用Albumentations库的数据增广&#xff0c;怎么把后者嵌入到前者的训练中。 其实也是比较简单…

CHS_02.1.4+操作系统体系结构 二

CHS_02.1.4操作系统体系结构 二 操作系统的结构 上篇文章我们只介绍过宏内核 也就是大内核以及微内核分层结构的操作系统模块化是一种很经典的程序设计思想宏内核和微内核外核 操作系统的结构 上篇文章我们只介绍过宏内核 也就是大内核以及微内核 今年大纲又增加了分层结构 模块…