HashMap扩展问题:HashMap如何实现线程安全?

news2024/9/23 3:31:18

HashMap如何实现线程安全?

方法一:java.util.Collections.synchronizedMap(Map<K,V> m)

底层实际上是将hashMap又封装了一层,变成SynchronizedMap<K,V>,并在每一个对HashMap的操作方法上添加了synchronized修饰。代码如下:(扩展:synchronized的原理是什么?synchronized和ReentrantLock有什么区别?)

private static class SynchronizedMap<K,V>
        implements Map<K,V>, Serializable {
        private static final long serialVersionUID = 1978198479659022715L;

        private final Map<K,V> m;     // Backing Map
        final Object      mutex;        // Object on which to synchronize

        SynchronizedMap(Map<K,V> m) {
            this.m = Objects.requireNonNull(m);
            mutex = this;
        }
        public Set<K> keySet() {
            synchronized (mutex) {
                if (keySet==null)
                    keySet = new SynchronizedSet<>(m.keySet(), mutex);
                return keySet;
            }
        }

        public Set<Map.Entry<K,V>> entrySet() {
            synchronized (mutex) {
                if (entrySet==null)
                    entrySet = new SynchronizedSet<>(m.entrySet(), mutex);
                return entrySet;
            }
        }
		//部分代码省略
}

方法二:ConcurrentHashMap

1. jdk1.8中数据结构由Node数组+链表实现,而1.7中数据结构由Segement数组+Entry数组+链表组成。

1.7:
1.7版本的ConcurrentHashMap采⽤了分段锁(Segment)技术,其中Segment继承了ReentrantLock。(扩展:AQS) 在插⼊ConcurrentHashMap元素时,先尝试获得Segment锁,先是⾃旋获锁,如果⾃旋次数超过阈值,则转为ReentrantLock上锁。

ConcurrentHashMap 的扩容是仅仅和每个Segment元素中HashEntry数组的⻓度有关,但需要扩容时,只扩容当前Segment中HashEntry数组即可。也就是说ConcurrentHashMap中Segment[]数组的⻓度是在初始化的时候就确定了,后⾯扩容不会改变这个⻓度。
在这里插入图片描述1.8:
1.8版本放弃了Segment,跟HashMap⼀样,⽤Node描述插⼊集合中的元素。但是Node中的val和next使⽤了volatile来修饰,保存了内存可⻅性。与HashMap相同的是,ConcurrentHashMap 1.8版本使⽤了数组+链表+红⿊树的结构。
同时,ConcurrentHashMap使⽤了CAS+Synchronized保证了并发的安全性。

对每一个node节点的head头节点加锁。Synchronized主要用于扩容,CAS用来做查找,替换,赋值等操作。

读操作无锁:Node节点的val和next通过volatile修饰。数组通过volatile修饰。保证了数据的可见性。
在这里插入图片描述为什么弃用Segement而用Synchroniized?
· 减少内存开销,如果使用ReentrantLock则需要节点继承AQS来获取同步支持,增加内存开销,而1.8只有头部节点需要进行同步

· 内部优化:synchronized是jvm直接支持的,jvm能够运行时做出相应的优化措施,锁粗化,锁消除,锁自旋,这使得synchronized能够随着JDK版本升级而不用改动代码前提下获得性能提升。

2.采用了 (h ^ (h >>> 16)) & HASH_BITS 计算节点的hash。

h ^ (h >>> 16) 参考这里。我们可以看到,ConcurrentHashMap中,相比于HashMap多了一个 & HASH_BITS ,这个计算保证了计算出来的Hash一定是一个正数。而要保证是正数的原因是因为负数在ConcurrentHashMap中存在特殊含义,通过保证正数来防止产生冲突。代码如下:

	/*
     * Encodings for Node hash fields. See above for explanation.
     */
    static final int MOVED     = -1; // 标识该节点正在进行复制/移动(也就是数组在扩容)
    static final int TREEBIN   = -2; // 用于该节点是红黑树中的节点
    static final int RESERVED  = -3; // 标识这个节点正在被重新计算 只用于ConcurrentHashMap.compute方法和ConcurrentHashMap.computeIfAbsent方法
    static final int HASH_BITS = 0x7fffffff; // 用于spread()方法计算哈希值。保证计算出来的hash是个正数

3.使用了helpTransfer方法帮助扩容。(扩展:ConcurrentHashMap如何实现帮助扩容的?)

4.使用了((long)i << ASHIFT) + ABASE来获取某个元素在数组中的值,涉及到偏移量的计算,原理是二分查找可以参考这里的 ASHIFT偏移量计算

通过计算

static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
    return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}

如果只是为了背书,那么你看到这里就可以了。因为下面的东西很繁琐又臭又长。写出来只是为了加深我自己的印象和理解。


首先我是从put方法往下捋的,在通过int hash = spread(key.hashCode());计算出哈希之后,进入循环将当前入参封装成Node节点塞值。

循环中分成了三种情况处理:

循环中情况1:

  • 当table==null,也就是还没有初始化的时候,那么先初始化。
    这里涉及到的参数是transient volatile int sizeCtl;sizeCtl为-1的时候表示正在执行初始化操作,sizeCtl>0的时候表示扩容的阈值(比如容量为16,扩容因子为0.75时,sizeCtl = 12)。在第一次初始化完,没有做任何操作的时候,sizeCtl是等于2的N次幂的(也就是数组的容量),但当执行过put操作之后,sizeCtl就会变成扩容的阈值而不是容量。
初始化同样分了两种情况:
初始化情况1:
  • 如果sizeCtl<0,那么表示当前ConcurrentHashMap已经有线程正在执行初始化操作,那么当前线程就直接yield(),
    你可能会奇怪那我这里暂停了之后,我这次操作的数据难道不插入了? (扩展:线程的几种状态)
    所以我试了一下在循环里yield,如下:
while (true){
   System.out.println("111");
   Thread.yield();
}

在这里插入图片描述
所以我们知道,循环并不会因为线程的yield而结束。因此,当前线程只是从运行回到了就绪,然后再次被CPU调用重新执行循环。

初始化情况2:
  • 如果sizeCtl>=0,那么首先通过CAS将sizeCtl设置为-1(与上一点对应)。然后初始化一个容量为16的数组,设置扩容阈值sizeCtl 为12。
    sc = n - (n >>> 2)在这里等价于 n*0.75(扩容因子)。
	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
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if ((tab = table) == null || tab.length == 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 - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

循环中情况2:

  • (f = tabAt(tab, i = (n - 1) & hash)判断当前节点计算出的索引在数组中是否已经存在其他节点,如果不存在那么通过CAS将当前节点塞进去。
    (n - 1) & hash计算出当前节点在数组中的位置。
    (f = tabAt(tab, i = (n - 1) & hash)是获取tab数组中 i位置的值。
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
    return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}

((long)i << ASHIFT) + ABASE含义是计算索引为i的元素在内存地址中的偏移量。

循环中情况3:

  • 如果索引位置已经存在了节点,那么判断该节点的状态是否是正在被移动或者复制(也就是判断当前ConcurrentHashMap是否正在进行扩容)。如果索引位置的节点处于移动或复制的状态下,那么当前线程帮助扩容。(扩展:ConcurrentHashMap如何实现帮助扩容的?)

循环中情况4:

  • 索引位置已经存在了节点,并且不处于扩容状态,那么通过synchronize锁这个节点,然后又有两个判断if (tabAt(tab, i) == f) 和 if (fh >= 0)(扩展:synchronize双重校验锁)。如果上述判断都满足,则执行插入操作。

    判断是否ConcurrentHashMap中已经存在了相同Key的Node,如果存在那么根据入参onlyIfAbsent(默认是false,也就是新值会替换旧值)判断是否覆盖值。

    如果不存在,那么将当前节点插入到索引位置的链表的最后一个元素。如果是二叉树步骤和链表一样。最后判断链表长度是否>8,如果大于8那么将链表转换为二叉树。

在 putVal 最后调用 addCount方法

addCount方法涉及到的就是CounterCell数组。
CounterCell的设计很巧妙,它的背后其实就是JDK1.8中的LongAdder。核心思想是:在并发较低的场景下直接采用baseCount累加,否则结合counterCells,将不同的线程散列到不同的cell中进行计算,尽可能地确保访问资源的隔离,减少冲突。LongAdder相比较于AtomicLong中无脑CAS的策略,在高并发的场景下,能够减少CAS重试的次数,提高计算效率。(这一句话是抄过来的哈哈 这个文章写的很好很好推荐!)

ConcurrentHashMap通过维护了一个CounterCell[]数组和BASECOUNT来得到Node数组中元素的个数,也就是ConcurrentHashMap.size()方法获取的值。addCount方法维护个数的同时,会重新校验是否需要扩容,并在需要扩容的情况下扩容。

在这里插入图片描述

x 表示这次需要在表中增加的元素个数,check 参数表示是否需要进行扩容检查,大于等于 0 都需要进行检查。
在 putVal 最后调用 addCount方法,传递了两个参数,分别是 1 和 binCount(链表长度)。

private final void addCount(long x, int check) {...}

size()方法就是获取BASECOUNT,并且在CounterCell[]数组不为空时遍历数组获取每一个value累加得到最后的Size。

public int size() {
    long n = sumCount();
    return ((n < 0L) ? 0 :
            (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
            (int)n);
}

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里面真的各种CAS各种锁标识+状态。一行一行看下来真的太费劲了。

简单归纳一下我学到了啥叭:

1.通过维护一个数组,将多个线程对一个值的CAS转成了对一个值+数组中各个节点的CAS来提高CAS的效率。(CounterCell[] as的作用)
2.分段锁思想。
3.使用CAS的流程,或者说加锁的流程,我们可以考虑从悲观锁到乐观锁,从粗粒度锁(源码中是synchronized锁Node节点),再到细粒度锁(CAS)。
4.偏移量那里我理解是为了加快CAS性能的,但是底层不明白,因为调用的是native方法,还需要再研究研究。

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

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

相关文章

基于 Webpack 插件体系的 Mock 服务

背景 在软件研发流程中&#xff0c;对于前后端分离的架构体系而言&#xff0c;为了能够更快速、高效的实现功能的开发&#xff0c;研发团队通常来说会在产品原型阶段对前后端联调的数据接口进行结构设计及约定&#xff0c;进而可以分别同步进行对应功能的实现&#xff0c;提升研…

WooCommerce Cost of Goods电商商城商品成本插件 轻松跟踪利润

WooCommerce Cost of Goods电商商城商品成本插件 轻松跟踪利润 WooCommerce Cost of Goods电商商城商品成本插件通过将货物成本纳入订单和报告中&#xff0c;轻松跟踪利润。 WooCommerce Cost of Goods电商商城商品成本插件功能 WooCommerce Cost of Goods电商商城商品成本插…

设计模式--工厂方法模式

实验3&#xff1a;工厂方法模式 本次实验属于模仿型实验&#xff0c;通过本次实验学生将掌握以下内容&#xff1a; 1、理解工厂方法模式的动机&#xff0c;掌握该模式的结构&#xff1b; 2、能够利用工厂方法模式解决实际问题。 [实验任务]&#xff1a;加密算法 目前常用…

IntelliJ IDEA插件

插件安装目录&#xff1a;C:\Users\<username>\AppData\Roaming\JetBrains\IntelliJIdea2021.2\plugins aiXcoder Code Completer&#xff1a;代码补全 Bookmark-X&#xff1a;书签分类 使用方法&#xff1a;鼠标移动到某一行&#xff0c;按ALT SHIFT D

静态HTTP:构建高效、可扩展的Web应用程序的基础

静态HTTP是Web应用程序的重要组成部分&#xff0c;它为构建高效、可扩展的Web应用程序提供了坚实的基础。下面将详细介绍静态HTTP的优势和在Web应用程序中的作用。 一、静态HTTP的优势 高效性能&#xff1a;静态HTTP内容在服务器上预先生成&#xff0c;然后通过HTTP协议传输到…

STM32MP157D-DK1开发板Qt镜像构建

上篇介绍了STM32MP57-DK1开发板官方系统的烧录。那个系统包含Linux系统的基础功能&#xff0c;如果要进行Qt开发&#xff0c;还需要重新构建带有Qt功能的镜像 本篇就来介绍如何构建带有Qt功能的系统镜像&#xff0c;并在开发板中烧录构建的镜像。 1 Distribution包的构建 ST…

Unity 如何获取当前日期的中文星期几

要获取当前日期是星期几可以使用DateTime下的DayOfWeek方法。 首先我们在脚本中添加System引用&#xff1a; using System; 然后我们再调用DateTime下的DayOfWeek方法&#xff1a; DayOfWeek dayOfWeek DateTime.Now.DayOfWeek; //获取当前是星期几 由于返回的是英文&…

simulink代码生成(三)——自定义变量名称

在simulink代码生成的学习过程中&#xff0c;遇到了一个卡壳的问题&#xff1a;如何在生成的代码中定义一个可控变量&#xff1f; 给大家看一下原m代码与生成的C代码对比结果&#xff1a; 原来的m函数代码&#xff1a;结构清晰&#xff0c;变量名与物理意义对应 生成的代码&a…

详解Java反射机制reflect(一学就会,通俗易懂)

1.定义 #2. 获取Class对象的三种方式 sout(c1)结果为class com.itheima.d2_reflect.TestClass 获取到了Class对象就相当于获取到了该类 2.获取类的构造器 3.获取全部构造器对象 2.根据参数类型获取构造器对象 类型后必须加.class 3.构造器对象调用构造器方法 4.暴力访问 4.获…

opencv入门到精通——图像平滑

目录 目标 2D卷积&#xff08;图像过滤&#xff09; 图像模糊&#xff08;图像平滑&#xff09; 1.平均 2.高斯模糊 3.中位模糊 4.双边滤波 目标 学会&#xff1a; 使用各种低通滤镜模糊图像 将定制的滤镜应用于图像&#xff08;2D卷积&#xff09; 2D卷积&#xff0…

【JavaScript】FileReader读取文件成功,但存储的数据为空——总结

目录 问题解决 问题 如题&#xff0c;使用下列代码读取上传的文件&#xff1a; for (let i 0; i < files.length; i) {const reader new FileReader();const fileName files[i].name;reader.onload function(e) {file_datas[fileName] e.target.result;}// 根据需要…

视频搜索AI平台,输入关键词全网查找相关内容

体验网站链接&#xff1a;https://avse.vercel.app GitHub网站链接&#xff1a;GitHub - yoeven/ai-video-search-engine 原文地址&#xff1a;视频搜索AI平台&#xff0c;输入关键词全网查找相关内容-喜好儿aigc 这个平台允许用户通过类似自然语言的查询方式搜索视频&#x…

UML建模(下午题)

内容概要 用例图 类图与对象图 顺序图 活动图 状态图 通讯图 试题一 试题二 来源于软件设计师学习视频&#xff08;仅供学习参考&#xff0c;附历年真题及详解&#xff09;_哔哩哔哩_bilibili的网课记

力扣经典面试题——搜索旋转排序数组及最小值(二分搜索旋转数组系列一次搞定)

我们先来看看一个常规的二分搜索是如何进行的&#xff1f; 例如要找一个有序数组的某个数 【1&#xff0c;2&#xff0c;4&#xff0c;5&#xff0c;9&#xff0c;11&#xff0c;15&#xff0c;19】 我们要找11&#xff0c;每次我们分割半边判断然后看到底在哪一边。 这里为什么…

【ASCII码】最完整详细介绍

目录 ASCII码的引入 ASCII码的表达方式 ASCII码解释 常见ASCII码的大小规则&#xff1a; 标准ASCII码&#xff08;128位&#xff09; 扩展ASCII码&#xff08;256位&#xff09; 参考资料 ASCII码的引入 在计算机中&#xff0c;所有的数据在存储和运算时都要使用二进制数…

前端H5实现微信授权

背景: 前段时间做了一个H5项目&#xff0c;H5项目需要放在微信公众号里面,并且需要通过微信授权拿到openId,所以就需要实现h5授权微信这个功能了。 原理: 其实原理就是前端在本项目首页去请求微信端提供的一个地址,并且在地址上配置微信所需要的参数,比如最重要的就是你要配…

每日一题——LeetCode160.相交链表

个人主页&#xff1a;白日依山璟 专栏&#xff1a;Java|数据结构与算法|每日一题 文章目录 1. 题目描述示例1&#xff1a;示例2&#xff1a;提示&#xff1a; 2. 思路3. 代码 1. 题目描述 给你两个单链表的头节点 headA 和 headB &#xff0c;请你找出并返回两个单链表相交的…

3.docker 安装失败

1、错误描述 2、报错前操作 ① 安装yum工具 yum install -y yum-utils \device-mapper-persistent-data \lvm2 --skip-broken ② 更新本地镜像源 # 设置docker镜像源 yum-config-manager \--add-repo \https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo…

改善OEE的关键:从反应性维护向预测性维护转变

科技的进步正在对企业的日常运营模式产生影响。许多制造企业已经采用了自动化生产流程&#xff0c;这不仅提高了产品质量&#xff0c;还简化了设备维护流程&#xff0c;并使得制造企业的设备维护方式从反应性维护转变为预测性维护。人们发现&#xff0c;设备维护方式的转变显著…

8.16 PowerBI系列之DAX函数专题-客户购买商品关联度的分析

需求 实现 1 客户数 countrows(values(sales[customerkey]))2 同时购买A和B的客户数 var A_cust values(sales[customerkey]) // var b_cust calculatetable(values(sales[customerkey]),usereletionship(product b[productkey],sales[productkey]),//激活虚拟关系all(p…