图解java.util.concurrent并发包源码系列——深入理解ConcurrentHashMap并发容器,看完薪水涨一千

图解java.util.concurrent并发包源码系列——深入理解ConcurrentHashMap并发容器

  • HashMap简单介绍
  • HashMap在并发场景下的问题
  • HashMap在并发场景下的替代方案
  • ConcurrentHashMap如何在线程安全的前提下提升并发度
    • 1.7
    • 1.8
  • JDK1.7的ConcurrentHashMap源码
  • JDK1.8的ConcurrentHashMap源码

HashMap简单介绍

ConcurrentHashMap是java.util.concurrent提供的一个并发安全的容器,可以实现高并发场景下读写的并发安全的同时兼顾了性能。它是HashMap的加强版,是并发安全的HashMap。

ConcurrentHashMap是基于HashMap的扩展,所以可以先简单回顾一下HashMap。

HashMap是一个存储键值对(key-value)的容器,往容器中放入元素要指定对应的key,往容器中获取元素前,通过指定key来获取对应的value。

HashMap里面使用一个数组去存放放入进来的键值对,在JDK1.7这个数组的类型是 Entry,而JDK1.8这个数组的类型变为Node。

当一对key-value要放入进来时,会计算当前要放入的数组下标。计算方式是取得key的hashcode,然后对hashcode使用hash函数进行运算,得到一个hash值,然后 hash & (数组长度 - 1) 计算出数组下标。然后把key-value封装为对应的实体类(Entry或Node),放入到数组中对应数组下标的位置上。

如果不同的元素放入数组是出现了hash碰撞,会采用链表的方式解决,在JDK1.8后,当链表长度大于等于8并且数组长度大于等于64,链表会转为红黑树。

HashMap内部记录了扩容阈值,当数组中元素的个数达到扩容阈值后,数组会进行扩容,并把元素重新散列到新数组中取。

HashMap的读取和写入都是简单以计算一个hash值,然后根据hash值计算数组下标,直接定位,所以时间复杂度都是O(1)。

在这里插入图片描述

HashMap在并发场景下的问题

HashMap是非删除安全的集合容器,在高并发场景下,会发生更新丢失的问题。比如当某个数组下标index对应的位置是空,此时两个线程同时调用put方法往HashMap中插入元素,而且正好都是插入到这个位置,它们如果同时判断当前位置是空,其中一个线程插入的元素就会被覆盖。

在这里插入图片描述

HashMap在并发场景下的替代方案

在ConcurrentHashMap出来以前,要解决并发场景下HashMap线程不安全的问题,可以使用Hashtable替代,Hashtable在所有方法上都加了synchronized关键字。

在这里插入图片描述

除了Hashtable以外,我们还可以使用Collections.synchronizedMap(hashMap)方法获得一个线程安全的Map容器。

java.util.Collections#synchronizedMap

    public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {return new SynchronizedMap<>(m);}

java.util.Collections.SynchronizedMap#SynchronizedMap(java.util.Map<K,V>)

        SynchronizedMap(Map<K,V> m) {this.m = Objects.requireNonNull(m);mutex = this;}

SynchronizedMap是Collections的内部类,保存了一个mutex作为锁对象,这个锁对象是this,也就是SynchronizedMap对象自己。而this.m保存的就是我们传递给Collections的Map。

java.util.Collections.SynchronizedMap#get

        public V get(Object key) {synchronized (mutex) {return m.get(key);}}

java.util.Collections.SynchronizedMap#put

        public V put(K key, V value) {synchronized (mutex) {return m.put(key, value);}}

SynchronizedMap的的方法都是先通过synchronized代码块保证并发安全,在操作我们的map之前,先获取mutex对象锁,然后在调我们的map的对应方法,是一种代理模式的实现。

在这里插入图片描述

这两种方式都是通过synchronized锁住一整个对象,虽然保证了线程安全,但是效率不高。所以JDK在1.5的版本推出了一个新的线程安全的并发Map集合ConcurrentHashMap。

ConcurrentHashMap如何在线程安全的前提下提升并发度

ConcurrentHashMap由于有1.7之前和1.8两个版本,所以要讨论ConcurrentHashMap如何在线程安全的前提下提升并发度,还要分开两个版本进行讨论。

1.7

JDK1.7的ConcurrentHashMap通过分段锁的机制提升并发度。

ConcurrentHashMap把原来HashMap的数组切分成一段一段,每一个段用一个Segment对象保存。当要往ConcurrentHashMap放入元素时,需要先定位元素在哪一个Segment中,然后定位到对应的Segment后,要获取ReentrantLock锁,加锁成功,才能往Segment里面的数组中插入元素。从ConcurrentHashMap中获取元素则不需要加锁,只需定位到对应的Segment,然后从Segment的数组中获取对应的元素。

ConcurrentHashMap结构:
在这里插入图片描述

写操作流程:

在这里插入图片描述

读操作流程:

在这里插入图片描述

1.8

JDK1.8的ConcurrentHashMap放弃了分段锁的思想,改用了synchronized加CAS实现。

ConcurrentHashMap的结构与HashMap一样,是一个Node数组。每次往Node数组写入数据前,先判断数组是否已经初始化,未初始化要先初始化,初始化要获取CAS自旋锁。数组已初始化,通过hash函数和下标计算定位写入的位置,判断该位置是否为null。如果为null,则通过CAS写入一个新的Node到该位置,如果CAS失败则自旋。如果对应的位置不是null,那么需要对当前位置的第一个Node加synchronized对象锁,加锁成功后才能遍历链表进行修改或新增操作(链表尾部)。由于JDK1.8的HashMap和ConcurrentHashMap都是尾插法,所以一旦一个数组位置中不为null,那么头节点是永远固定的。而从ConcurrentHashMap中读取某个元素时,是不需要加锁的,而且由于没有分段,所以不需要像1.7那样两次定位,所以读操作的流程与HashMap是基本一样的。

ConcurrentHashMap结构:

在这里插入图片描述

写操作流程:
在这里插入图片描述

JDK1.7的ConcurrentHashMap源码

ConcurrentHashMap内部有一个Segment的数组。

final Segment<K,V>[] segments;

每个Segment内部又有一个HashEntry数组。

transient volatile HashEntry<K,V>[] table;

Segment继承了ReentrantLock锁,可以通过Segment加锁。

static final class Segment<K,V> extends ReentrantLock implements Serializable {...}

ConcurrentHashMap#put:

    public V put(K key, V value) {Segment<K,V> s;if (value == null)throw new NullPointerException();// 通过hash函数计算出hash值int hash = hash(key.hashCode());// 定位Segmentint j = (hash >>> segmentShift) & segmentMask;if ((s = (Segment<K,V>)UNSAFE.getObject(segments, (j << SSHIFT) + SBASE)) == null)s = ensureSegment(j);// 调用Segment的put方法return s.put(key, hash, value, false);}
  1. 通过hash函数计算出hash值
  2. 定位Segment
  3. 调用Segment的put方法

在这里插入图片描述

Segment#put:

        final V put(K key, int hash, V value, boolean onlyIfAbsent) {// 获取ReentrantLock锁HashEntry<K,V> node = tryLock() ? null :scanAndLockForPut(key, hash, value);V oldValue;try {HashEntry<K,V>[] tab = table;// 定位数组下标int index = (tab.length - 1) & hash;// 数组下标对应的位置的第一个节点HashEntry<K,V> first = entryAt(tab, index);for (HashEntry<K,V> e = first;;) {// 遍历链表if (e != null) {K k;// 找到匹配的key,修改value值if ((k = e.key) == key ||(e.hash == hash && key.equals(k))) {oldValue = e.value;if (!onlyIfAbsent) {e.value = value;++modCount;}break;}e = e.next;}else {// 遍历到最后,没有发现匹配的keyif (node != null)// 头插法node.setNext(first);else// 目标位置为null,new一个HashEntrynode = new HashEntry<K,V>(hash, key, value, first);int c = count + 1;// 如果元素个数大于扩容阈值,进行扩容if (c > threshold && tab.length < MAXIMUM_CAPACITY)rehash(node);else// 插入到数组中setEntryAt(tab, index, node);++modCount;count = c;oldValue = null;break;}}} finally {// 释放锁unlock();}return oldValue;}
  1. 获取ReentrantLock锁
  2. 定位数组下标 (tab.length - 1) & hash
  3. 获取数组下标对应的位置的第一个元素,遍历链表
  4. 找到匹配的key,修改value值
  5. 遍历到最后,没有发现匹配的key
    • 5.1 目标位置是null,new一个HashEntry
    • 5.2 目标位置不是null,头插法
    • 5.3 如果元素个数大于扩容阈值,进行扩容
  6. 释放锁

在这里插入图片描述

ConcurrentHashMap#get:

    public V get(Object key) {Segment<K,V> s;HashEntry<K,V>[] tab;// 通过hash函数获取hash值int h = hash(key.hashCode());// 定位Segmentlong u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;// 通过UNSAFE.getObjectVolatile方法取得Segmentif ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&(tab = s.table) != null) {// (tab.length - 1) & h 定位数组位置,遍历链表for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);e != null; e = e.next) {K k;// 找到匹配key的HashEntry,返回valueif ((k = e.key) == key || (e.hash == h && key.equals(k)))return e.value;}}return null;}
  1. 通过一个hash函数,取得一个hash值h
  2. 用h进行位运算取得Segment数组中的目标位置u
  3. 通过UNSAFE.getObjectVolatile(segments, u)取得目标Segment
  4. 通过 (tab.length - 1) & h 计算得到HashEntry数组中的目标位置
  5. 遍历链表,找到匹配key的HashEntry,返回value

在这里插入图片描述

JDK1.8的ConcurrentHashMap源码

java.util.concurrent.ConcurrentHashMap#put:

    public V put(K key, V value) {return putVal(key, value, false);}

java.util.concurrent.ConcurrentHashMap#putVal:

    final V putVal(K key, V value, boolean onlyIfAbsent) {if (key == null || value == null) throw new NullPointerException();// 通过hash函数得到hash值int hash = spread(key.hashCode());int binCount = 0;for (Node<K,V>[] tab = table;;) {Node<K,V> f; int n, i, fh;if (tab == null || (n = tab.length) == 0)// 如果数组未初始化,先初始化数组tab = initTable();else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {// 数组目标位置为null,CAS插入if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))break;}// 数组正在扩容,参与数组扩容else if ((fh = f.hash) == MOVED)tab = helpTransfer(tab, f);else {V oldVal = null;// 需要遍历链表,先对链表头节点加synchronized锁synchronized (f) {if (tabAt(tab, i) == f) {if (fh >= 0) {binCount = 1;// 遍历链表for (Node<K,V> e = f;; ++binCount) {K ek;// 如果找到匹配key的Node,修改valueif (e.hash == hash &&((ek = e.key) == key ||(ek != null && key.equals(ek)))) {oldVal = e.val;if (!onlyIfAbsent)e.val = value;break;}Node<K,V> pred = e;// 遍历到链表尾部,插入新节点到尾部if ((e = e.next) == null) {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;}}}}if (binCount != 0) {// 如果链表长度大于等于8,数组长度大于等于64,链表转红黑树,数组长度不够64,数组扩容if (binCount >= TREEIFY_THRESHOLD)treeifyBin(tab, i);if (oldVal != null)return oldVal;break;}}}// 增加元素计算,并判断是否需要扩容addCount(1L, binCount);return null;}
  1. 通过hash函数得到hash值
  2. 如果数组未初始化,先初始化数组
  3. 数组目标位置为null,尝试CAS插入新节点到目标位置
  4. 如果数组正在扩容,参与数组扩容
  5. 如果需要遍历链表,先对链表头节点加synchronized锁
  6. 遍历链表
    • 6.1 如果找到匹配key的Node,修改value
    • 6.2 遍历到链表尾部,插入新节点到尾部
  7. 如果链表长度大于等于8,数组长度大于等于64,链表转红黑树,数组长度不够64,数组扩容
  8. 增加元素计算,并判断是否需要扩容

在这里插入图片描述

java.util.concurrent.ConcurrentHashMap#initTable

    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();// CAS获取自旋锁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数组Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];table = tab = nt;sc = n - (n >>> 2);}} finally {sizeCtl = sc;}break;}}return tab;}

initTable方法进行Node数组的初始化,初始化前先通过CAS获取自旋锁,获取到了才能进行Node数组的初始化。

在这里插入图片描述

java.util.concurrent.ConcurrentHashMap#casTabAt:

    static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,Node<K,V> c, Node<K,V> v) {return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);}

casTabAt是当数组中对应位置元素为null时调用的,尝试CAS初始化对应位置的元素,调用的是Unsafe的compareAndSwapObject方法。

在这里插入图片描述

java.util.concurrent.ConcurrentHashMap#get

    public V get(Object key) {Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;// 通过hash函数获取hash值int h = spread(key.hashCode());// tabAt(tab, (n - 1) & h) 计算数组下标if ((tab = table) != null && (n = tab.length) > 0 &&(e = tabAt(tab, (n - 1) & h)) != null) {// 第一个就是匹配key的Node,直接取value值if ((eh = e.hash) == h) {if ((ek = e.key) == key || (ek != null && key.equals(ek)))return e.val;}// 数组在扩容的时候,有可能会进这个分支,如果进了这个分支,代表当前位置的元素已经全被挪到新数组中去了,到新数组中去找else if (eh < 0)return (p = e.find(h, key)) != null ? p.val : null;// 遍历链表,找到匹配的key,取value值while ((e = e.next) != null) {if (e.hash == h &&((ek = e.key) == key || (ek != null && key.equals(ek))))return e.val;}}return null;}
  1. 通过hash函数获取hash值
  2. tabAt(tab, (n - 1) & h) 计算数组下标
  3. 如果第一个就是匹配key的Node,直接取value值
  4. 遍历链表,找到匹配的key,取value值

在这里插入图片描述

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

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

相关文章

网络流学习笔记

网络流基础 基本概念 源点&#xff08;source&#xff09; s s s&#xff0c;汇点 t t t。 容量&#xff1a;约等于边权。不存在的边流量可视为 0 0 0。 ( u , v ) (u,v) (u,v) 的流量通常记为 c ( u , v ) c(u,v) c(u,v)&#xff08;capacity&#xff09;。 流&#xff…

解决MySQL大版本升级导致.Net(C#)程序连接报错问题

数据库版本从MySQL 5.7.21 升级到 MySQL8.0.21 数据升级完成后&#xff0c;直接修改程序的数据库连接配置信息 <connectionStrings> <add name"myConnectionString" connectionString"server192.168.31.200;uidapp;pwdFgTDkn0q!75;databasemail;&q…

【语义分割】语义分割概念及算法介绍

文章目录 一、基本概念二、研究现状2.1 传统算法2.2 深度学习方法 三、数据集及评价指标3.1 常用数据集3.2 常用指标 四、经典模型参考资料 一、基本概念 语义分割是计算机视觉中很重要的一个方向。不同于目标检测和识别&#xff0c;语义分割实现了图像像素级的分类。它能够将…

使用langchain-chatchat里,faiss库中报错: AssertionError ,位置:assert d == self.d

发生报错&#xff1a; AssertionError&#xff0c;发生位置&#xff1a;class_wrappers.py里 assert d self.d&#xff0c;假如输出语句&#xff0c;查看到是因为d和self.d维度不匹配造成&#xff0c;解决方式&#xff1a; 删除langchain-chatchat/knowledge_base里的info.db…

【iOS免越狱】利用IOS自动化web-driver-agent_appium-实现自动点击+滑动屏幕

1.目标 在做饭、锻炼等无法腾出双手的场景中&#xff0c;想刷刷抖音 刷抖音的时候有太多的广告 如何解决痛点 抖音自动播放下一个视频 iOS系统高版本无法 越狱 安装插件 2.操作环境 MAC一台&#xff0c;安装 Xcode iPhone一台&#xff0c;16 系统以上最佳 3.流程 下载最…

Python 算法高级篇:堆排序的优化与应用

Python 算法高级篇&#xff1a;堆排序的优化与应用 引言 1. 什么是堆&#xff1f;2. 堆的性质3. 堆排序的基本原理4. 堆排序的 Python 实现5. 堆排序的性能和优化6. 堆排序的实际应用7. 总结 引言 堆排序是一种高效的排序算法&#xff0c;它基于数据结构中的堆这一概念。堆排序…

C++进阶语法——OOP(面向对象)【学习笔记(四)】

文章目录 1、C OOP⾯向对象开发1.1 类&#xff08;classes&#xff09;和对象&#xff08;objects&#xff09;1.2 public、private、protected访问权限1.3 实现成员⽅法1.4 构造函数&#xff08;constructor&#xff09;和 析构函数&#xff08;destructor&#xff09;1.4.1 构…

Java基础 多线程

1.多线程创建方式1&#xff0c;继承Thread类&#xff1a; 2.多线程创建方式2&#xff1a; 匿名内部类写法 package thread;public class ThreadTest {public static void main(String[] args) {Runnable runnable new Runnable() {Overridepublic void run() {for (int i 0…

笔记本电脑的摄像头找不到黑屏解决办法

这种问题一般来说就是缺少驱动&#xff0c;就要下载驱动。 问题&#xff1a; 解决办法&#xff1a; 1.进入联想官网下载驱动 网站&#xff1a;https://newsupport.lenovo.com.cn/driveDownloads_index.html?v9d9bc7ad5023ef3c3d5e3cf386e2f187 2.下载主机编号检测工具 3.下…

虚幻中的网络概述一

前置&#xff1a;在学习完turbo强大佬的多人fps之后发觉自己在虚幻网络方面还有许多基础知识不太清楚&#xff0c;结合安宁Ken大佬与虚幻官方文档进行补足。 补充&#xff1a;官方文档中的描述挺好的&#xff0c;自己只算是搬运和将两者结合加强理解。 学习虚幻中的网络先从虚…

【Docker】Python Flask + Redis 练习

一、构建flask镜像 1.准备文件 创建app.py,内容如下 from flask import Flask from redis import Redis app Flask(__name__) redis Redis(hostos.environ.get(REDIS_HOST,127.0.0.1),port6379)app.route(/) def hello():redis.incr(hits)return f"Hello Container W…

串行原理编程,中文编程工具中的串行构件,串行连接操作简单

串行通信原理编程&#xff0c;中文编程工具中的串行通信构件&#xff0c;串行通信连接设置简单 编程系统化课程总目录及明细&#xff0c;点击进入了解详情。https://blog.csdn.net/qq_29129627/article/details/134073098?spm1001.2014.3001.5502 串行端口 是串行的基础&#…

【C++】类与对象 第二篇(构造函数,析构函数,拷贝构造,赋值重载)

目录 类的6个默认成员函数 初始化和清理 1.构造函数 2.析构函数 3.共同点 拷贝复制 1.拷贝构造 使用细节 2.赋值重载 运算符重载 < < > > ! 连续赋值 C入门 第一篇(C关键字&#xff0c; 命名空间&#xff0c;C输入&输出)-CSDN博客 C入门 第二篇( 引…

【开源】基于SpringBoot的海南旅游景点推荐系统的设计和实现

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 用户端2.2 管理员端 三、系统展示四、核心代码4.1 随机景点推荐4.2 景点评价4.3 协同推荐算法4.4 网站登录4.5 查询景点美食 五、免责说明 一、摘要 1.1 项目介绍 基于VueSpringBootMySQL的海南旅游推荐系统&#xff…

2017年上半年上午易错题(软件设计师考试)

CPU 执行算术运算或者逻辑运算时&#xff0c;常将源操作数和结果暂存在&#xff08; &#xff09;中。 A &#xff0e; 程序计数器 (PC) B. 累加器 (AC) C. 指令寄存器 (IR) D. 地址寄存器 (AR) 某系统由下图所示的冗余部件构成。若每个部件的千小时可靠度都为 R &…

如何使用手机蓝牙设备作为电脑的解锁工具像动态锁那样,蓝牙接近了电脑,电脑自动解锁无需输入开机密码

环境&#xff1a; Win10 专业版 远程解锁 蓝牙解锁小程序 问题描述&#xff1a; 如何使用手机蓝牙设备作为电脑的解锁工具像动态锁那样&#xff0c;蓝牙接近了电脑&#xff0c;电脑自动解锁无需输入开机密码 手机不需要拿出来&#xff0c;在口袋里就可以自动解锁&#xff…

C#,数值计算——分类与推理,基座向量机的 Svmgenkernel的计算方法与源程序

1 文本格式 using System; namespace Legalsoft.Truffer { public abstract class Svmgenkernel { public int m { get; set; } public int kcalls { get; set; } public double[,] ker { get; set; } public double[] y { get; set…

机器学习-特征选择:如何使用互信息特征选择挑选出最佳特征?

一、引言 特征选择在机器学习中扮演着至关重要的角色&#xff0c;它可以帮助我们从大量的特征中挑选出对目标变量具有最大预测能力的特征。互信息特征选择是一种常用的特征选择方法&#xff0c;它通过计算特征与目标变量之间的互信息来评估特征的重要性。 互信息是信息论中的一…

Csdn文章编写参考案例

这里写自定义目录标题 欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants 创建一个自定义列表如何创建一个…

cosover是什么?crossover23又是什么软件

cosover是篮球里的过人技巧。 1.crossover在篮球中的本意是交叉步和急速交叉步。crossover 是篮球术语&#xff0c;有胯下运球、双手交替运球&#xff0c;交叉步过人、急速大幅度变向等之意。 2.在NBA里是指包括胯下运球、变向、插花在内的过人的技巧。 NBA有很多著名的Cross…