并发编程系列之并发容器:ConcurrentHashMap

2023-10-31

前言

之前我们讲了线程,锁以及队列同步器等等一些并发相关底层的东西,当然Java开发者在开发中很少直接去使用之前所讲的,因为Java为了简化开发,为我们提供了一整套并发容器和框架,但是这些容器和框架都是建立在之前所讲的基础之上的,今天就让我们来看第一个并发容器:ConcurrentHashMap,我们主要从原理和使用两个方面介绍,让我们扬帆起航,开始今天的并发之旅吧。

 

景点一:为什么要使用ConcurrentHashMap

ConcurrentHashMap是一个线程安全并且高效的HashMap,基于下面两点我们还是在并发场景中优先考虑ConcurrentHashMap。

  • 线程不安全的HashMap:在多线程环境下,使用HashMap进行操作会引起死循环,导致CPU100%甚至服务器之间崩溃,读者可以参考下面代码自己试一下,(亲测使用Hash服务器直接卡死,不信U CAN TRY):多线程会导致HashMap的Entry链表形成环形数据结构,一旦形成环,那么Entry的next节点永远不为空,Hash就会陷入死循环获取Entry的场景

public class ConcurrentMapDemo {
   public static void main(String[] args) throws InterruptedException {
      // final HashMap<String, String> map = new HashMap<String, String>();
      final ConcurrentHashMap map = new ConcurrentHashMap<>();
       Thread t1 = new Thread(new Runnable() {
           @Override
           public void run() {
               for (int i = 0; i < 1000; i++) {
                   new Thread(new Runnable() {
                       @Override
                       public void run() {
                           for (int i = 0; i < 1000; i++) {
                               map.put(UUID.randomUUID().toString(), "");
                           }
                       }
                   }, "t2").start();
               }
           }
       }, "t1");
       t1.start();
   }
}
  • 效率低下的HashTable:HashTable是使用synchronized来保证线程安全,但是在多线程并发环境下线程竞争激烈,HashTable的效率非常低,也正是因为synchronized导致每次只能有一个线程访问同步块,其他线程处于阻塞或者轮询状态,根本无法对同步块进行任何操作,所以线程竞争越激烈(线程数量越多),效率越低,这明显不满足我们使用多线程的初衷;

  • ConcurrentHashMap锁分段技术:ConcurrentHashMap内部使用段(segment)来表示这些不同的部分,每个段其实就是一个小的HashTable,他们有自己各自的锁,只要多个修改操作发生在不同的段上,他们之间就可以并发的进行;

 

景点二:ConcurrentHashMap的结构

通过上面的结构图,我们来分析下:ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成,Segment是一种可重入锁,在ConcurrentHashMap中充当锁的角色,HashEntry是用来存储键值对数据。这三者的关系如下图:

 

景点三:ConcurrentHashMap的底层实现

ConcurrentHashMap把一个整体分成16个段(segment),也就是说最高支持16个线程的并发修改操作。这也是在多线程场景下减小锁粒度从而降低竞争的一种方案。并且代码中大多共享变量使用volatile关键字,目的是第一时间获取修改的内容,性能非常好。

ConcurrentHashMap的底层主要包括初始化Segment数组、SegmentShift、SegmentMask和每个Segment里面的HashEntry数组以及Segment的定位,下面我们将来一一分析:

初始化Segment数组

if (concurrencyLevel > MAX_SEGMENTS)
           concurrencyLevel = MAX_SEGMENTS;
       // segments数组的长度ssize通过concurrencyLevel计算得出。
       //为了能通过按位与的哈希算法来定位segments数组的索引,
       //必须保证segments数组的长度是2的N次方
       // 所以必须计算出一个是大于或等于concurrencyLevel的最小的2的N次方值来作为segments数组的长度
       int sshift = 0;
       int ssize = 1;
       while (ssize < concurrencyLevel) {
           ++sshift;
           ssize <<= 1;
       }
       this.segmentShift = 32 - sshift;
       this.segmentMask = ssize - 1;

初始化SegmentShift和SegmentMask:这两个全局变量在定位segment时的哈希散列中使用,sshift等于ssize从1向左移位的次数,在默认情况下concurrencyLevel等于16,1需要向左移位移动4次,所以sshift等于4。segmentShift用于定位参与hash运算的位数,segmentShift等于32减sshift,所以等于28,这里之所以用32是因为ConcurrentHashMap里的hash()方法输出的最大数是32位的。

segmentMask是哈希运算的掩码,等于ssize减1,即15,掩码的二进制各个位的值都是1。因为ssize的最大长度是65536,所以segmentShift最大值是16,segmentMask最大值是65535,对应的二进制是16位,每个位都是1;

初始化每个Segment里面的HashEntry:

// initialCapacity是ConcurrentHashMap的初始化容量
// loadFactor是每个Segment的负载因子
// cap为Segment里面HashEntry数组的长度,它等于initialCapacity/ssize的倍数c,如果c>1则就会取大于等于c的2的N次方值
// 所以cap如果不是1就是2的N次方。
// Segment的容量=(int)cap*loadFactor,默认情况下loadFactor=0.75,initialCapacity=16
if (initialCapacity > MAXIMUM_CAPACITY)
           initialCapacity = MAXIMUM_CAPACITY;
       int c = initialCapacity / ssize;
       if (c * ssize < initialCapacity)
           ++c;
       int cap = MIN_SEGMENT_TABLE_CAPACITY;
       while (cap < c)
           cap <<= 1;
       // create segments and segments[0]
       Segment<K,V> s0 = new Segment<K,V>(loadFactor, (int)(cap * loadFactor),(HashEntry<K,V>[])new HashEntry[cap]);
       Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];

定位Segment:ConcurrentHashMap使用分段锁Segment来保护每段数据,那么在插入和获取元素的时候,必须先通过哈希算法定位到Segment,ConcurrentHashMap会对元素的HashCode在进行一次hash散列,进行2次哈希的目的是减少散列冲突,使元素能够均匀地分布在不同的Segment上,提高容器的存取效率。

如果哈希散列最坏的情况是所有元素元素全部散落在一个Segment中,那么分段锁的意义就没有了,而且存取效率也极差,所以为了尽量减少散列冲突ConcurrentHashMap是通过2次哈希来做的,我们可以看具体源码如下:

// segmentShift默认情况下=28
// segmentMask默认情况下=15
private Segment<K,V> segmentForHash(int h) {
       // hash值右移28位(让高4位参与到散列中) 再和segmentShift做与运算
       long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
       return (Segment<K,V>) UNSAFE.getObjectVolatile(segments, u);
   }

 

景点四:ConcurrentHashMap的操作

ConcurrentHashMap的的方法有很多如下,我们主要讲三种:get、put和size

put操作

调用特别简单跟使用HashMap没有任何区别,如下:

ConcurrentHashMap concurrentHashMap = new ConcurrentHashMap<>();
       concurrentHashMap.put("Justin","Justin的后端书架");

但是,由于ConcurrentHashMap是线程安全的所以put方法需要对共享变量进行写入操作,所以为了线程安全,必须加锁,我们看下put的底层实现:

/**
* put方法首先定位到Segment,然后在Segment里面进行插入操作
* 插入操作需要进行2步:
* 第一步:先判断是否需要对Segment里的HashEntry进行扩容,判断HashEntry是否
*        是否超过容量threshold,如果超过阈值则进行扩容,Segment的扩容比
*        Hashmap的扩容更合理,Hash是在插入元素之后再判断是否需要进行扩容,
*        如果插入元素之后,满足扩容条件,但是后续没有元素新增,那就会做了一次无效的扩容
*  拓展:如何扩容?扩容的时候首先会创建一个两倍于原容量的数组,然后将原数组里的元素进行再hash后插入到新的数组里。
*      为了高效ConcurrentHashMap不会对整个容器进行扩容,而只对某个segment进行扩容
* 第二步是定位到添加元素的位置,然后将它放入HashEntry数组里面
*/
public V put(K key, V value) {
       Segment<K,V> s;
       if (value == null)
           throw new NullPointerException();
       int hash = hash(key);
       int j = (hash >>> segmentShift) & segmentMask;
       if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
            (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
           s = ensureSegment(j);
       return s.put(key, hash, value, false);
   }

get操作

使用很简单:

ConcurrentHashMap concurrentHashMap = new ConcurrentHashMap<>();
       concurrentHashMap.put("Justin","Justin的后端书架");
       System.out.println(concurrentHashMap.get("Justin"));

我们再看下源码实现:

/**
* 三步走:第一步,先对key经过一次再散列
*        第二步:使用这个散列值通过散列运算定位到Segment
*        第三步:再通过散列算法定位到元素
*/
public V get(Object key) {
       Segment<K,V> s;
       HashEntry<K,V>[] tab;
       int h = hash(key);
       long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
       if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
           (tab = s.table) != null) {
           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;
               if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                   return e.value;
           }
       }
       return null;
   }

get操作的高效之处在于整个get过程不需要加锁,除非读到的值是空的才会加锁重读,那么我们看下ConcurrentHashMap是如何做到get不加锁的吧:原因是它的get方法里将要使用的共享变量都定义成volatile,在get操作里只需要读不需要写共享变量,所以可以不用加锁。之所以不会读到过期的值,是因为根据JMM的先行发生原则,对volatile字段的写入操作优先于读操作,即使两个线程同时修改和获取volatile变量,get操作最终也只会拿到最新的值。

size操作

我们要统计ConcurrentHashMap里元素的大小,就必须统计所有的分区Segment里面元素的总和,但是如果我们在统计每个Segment里面元素总数的过程中,之前已经统计过得Segment又发生了更新,那么之前统计的总数就失效了,所以最安全的做法是在统计每个Segment的时候统计好的Segment就将他的更新操作全部锁住,等待全部Segment统计完毕再释放,但是显然这样是不科学的,效率非常低下。

源码如下:

/**
 * 在累加count操作过程中,之前统计过的Segment发生变化的几率比较小,所以
 * ConcurrentHashMap的做法是先尝试2次不加锁的方式来统计各个Segment大小,如果统计的过程中,
 * 容器的count发生了变化,则再采用加锁的方式来统计所有Segment的大小
 * ConcurrentHashMap是如何判断在统计的时候容器是否发生了变化呢?
 * 使用modCount变量,在put , remove和clean方法里操作元素前都会将变量modCount进行加1,
 * 那么在统计size前后比较modCount是否发生变化,从而得知容器的大小是否发生变化
 */
public int size() {
       final Segment<K,V>[] segments = this.segments;
       int size;
       boolean overflow; // true if size overflows 32 bits
       long sum;         // sum of modCounts
       long last = 0L;   // previous sum
       int retries = -1; // first iteration isn't retry
       try {
           for (;;) {
               if (retries++ == RETRIES_BEFORE_LOCK) {
                   for (int j = 0; j < segments.length; ++j)
                       ensureSegment(j).lock(); // force creation
               }
               sum = 0L;
               size = 0;
               overflow = false;
               for (int j = 0; j < segments.length; ++j) {
                   Segment<K,V> seg = segmentAt(segments, j);
                   if (seg != null) {
                       sum += seg.modCount;
                       int c = seg.count;
                       if (c < 0 || (size += c) < 0)
                           overflow = true;
                   }
               }
               if (sum == last)
                   break;
               last = sum;
           }
       } finally {
           if (retries > RETRIES_BEFORE_LOCK) {
               for (int j = 0; j < segments.length; ++j)
                   segmentAt(segments, j).unlock();
           }
       }
       return overflow ? Integer.MAX_VALUE : size;
   }

 

景点五:ConcurrentHashMap的缺点

ConcurrentHashMap的缺点就是他最多只能支持16个线程的并发,如果实际场景中,你需要启动的线程的数量比较多,还是同样会发生锁竞争和等待的问题:

 

以上就是今天所讲的ConcurrentHashMap的五大景点的所有内容,希望能对你有所帮助,通过短短十几分钟的阅读,希望你能有所收获!!!

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

并发编程系列之并发容器:ConcurrentHashMap 的相关文章

  • 【Java并发编程的艺术】Java并发容器和框架:Java中的阻塞队列

    1 什么是阻塞队列 阻塞队列常用于生产者和消费者的场景 生产者是向队列里添加元素的线程 消费者是 从队列里取元素的线程 阻塞队列就是生产者用来存放元素 消费者用来获取元素的容器 阻塞队列 BlockingQueue 是一个支持两个附加操作的
  • Cuda Streams的概述(一)-- Cuda介绍

    最近在做有关Cuda的一个项目 碰到匪夷所思的问题 在异步的时候发现并没有达到预期的效果 程序没有异步起来 然后在网上找了一个Nvida的有关Cuda Streams的一个ppt 然后照着里面的提示 使程序达到了异步的效果 首先 先回顾一下
  • java中Synchronized和Lock的区别

    Synchronized和Lock的区别 原始构成 synchronized关键字属于JVM层面的 通过monitorenter monitorexit指令实现 底层是通过monitor对象来完成 其实wait notify等方法也依赖mo
  • 并发编程系列——6线程池核心原理分析

    学习目标 线程池的作用 jdk给我们提供了哪几种常用线程池 线程池有哪几大核心参数 线程池的拒绝策略有哪些 线程中阻塞队列的作用 线程池的工作流程 线程池的设计思维 线程池中的阻塞队列如果用默认的 会有哪些问题 线程池的工作状态有哪些 线程
  • 多线程-Thread类的常用方法及使用场景

    众所周知 操作线程就必须熟读线程的API方法 万一你开个多线程刹不住车就歇菜了 下面就介绍一些API基本用法 包括sleep join yield interrupt sleep 让当前线程睡一会 原生用法Thread sleep 毫秒 会
  • 场景题之最快返回结果

    场景题之最快返回结果 问题描述 输入中文 最快从百度翻译 谷歌翻译 有道翻译获取结果返回 代码实现 思路 采用CompletableFuture实现 多个CompletableFuture可以串行执行 也可以并行执行 其中anyOf 方法只
  • 面试官:说说CountDownLatch,CyclicBarrier,Semaphore的原理?

    CountDownLatch CountDownLatch适用于在多线程的场景需要等待所有子线程全部执行完毕之后再做操作的场景 举个例子 早上部门开会 有人在上厕所 这时候需要等待所有人从厕所回来之后才能开始会议 public class
  • 什么是ConcurrentHashMap?

    文章目录 什么是ConcurrentHashMap ConcurrentHashMap 的主要特性 ConcurrentHashMap 的用法 ConcurrentHashMap 的实现原理 在什么场景使用ConcurrentHashMap
  • Callable和Future原理解析

    首先进行分析前 我们需要了解到的概念 Callable是一个接口 是用于创建线程执行里面有一个call方法 用于线程执行的内容 由业务自己定义 Future也是一个接口 可以异步的通过get方法获取到call返回的内容 比较常见的使用场景
  • 上下文切换理解以及减少方法

    并发编程面临着上下文切换 死锁等问题 尤其在少量数据的情况下 并发可能因为线程的创建和上下文切换的开销等问题 甚至比串行执行的速度更慢 文章目录 上下文切换定义 例子理解 减少上下文切换的方法 无锁并发编程 CAS算法 使用最少线程 协程
  • 【并发】并发

    并发 进程和线程 进程 资源分配的基本单位 可以理解为在内存中运行的程序 每个进程都有独立的内存空间 一个进程包含多个线程 线程 任务执行的基本单位 负责进程中任务的执行 每个线程共享进程的内存空间 一个线程使用时 其他线程必须等待 用户
  • Java并发之锁

    Java并发之锁 一 临界区 二 线程安全 三 解决临界区线程安全问题 四 Java对象头 五 重量级锁 Monitor 5 1 synchronized 5 1 1 synchronized加锁流程 六 轻量级锁 6 1 轻量级锁加锁流程
  • 高并发,你真的理解透彻了吗?

    高并发 几乎是每个程序员都想拥有的经验 原因很简单 随着流量变大 会遇到各种各样的技术问题 比如接口响应超时 CPU load升高 GC频繁 死锁 大数据量存储等等 这些问题能推动我们在技术深度上不断精进 在过往的面试中 如果候选人做过高并
  • CUDA编程问题记录:能否用CPU多线程调用CUDA核函数

    问题 能否在主机端创建CPU多线程 在每个线程里调用设备端核函数的caller函数 进而实现进一步的并行运行 例如有5张图片 对于每张图片都有N个GPU线程对其进行像素操作 但是此时是逐一对这5张图片处理的 想在主机端创建5个CPU线程 每
  • synchronized的作用和用法

    郁闷 参考 synchronized的作用和用法 Java中Synchronized的使用 文章目录 简单介绍 用法 实战实例 修饰代码块 修饰普通方法 修饰静态方法 简单介绍 synchronized关键字是用来控制线程同步的 就是在多线
  • 13张图,带大家深入理解Synchronized

    目录 前言 内容大纲 Synchronized使用方式 普通函数 静态函数 代码块 Synchronized原理 Synchronized优化 锁粗化 锁消除 锁升级 偏向锁 轻量级锁 重量级锁 前言 Java并发编程系列第二篇Synchr
  • JUC编程

    1 JUC JUC就是java util concurrent工具包的简称 这是一个处理线程的工具包 JDK 1 5开始出现的 1 传统的synchronized public class Synchronized public stati
  • 接口并发性能测试开发之:从测试方案设计、测试策略、指标分析到代码编写,这一篇全搞定。

    并发接口性能设计思路与代码编写 1 引言 2 并发测试定义 3 并发测试分类 4 设计思路整理 5 测试方案设计 6 指标分析 7 代码实战 8 总结 1 引言 这篇是我3月份在公司内部做的技术分享内容 由于我在公司内部分享的内容较多 以及
  • Java并发编程之设计模式

    同步模式之保护性暂停 1 定义 即 Guarded Suspension 用在一个线程等待另一个线程的执行结果 要点 有一个结果需要从一个线程传递到另一个线程 让他们关联同一个 GuardedObject 如果有结果不断从一个线程到另一个线
  • 并发编程 (6)一不小心就死锁了,怎么办?

    在上一篇文章中 我们用 Account class 作为互斥锁 来解决银行业务里面的转账问题 虽然这个方案不存在并发问题 但是所有账户的转账操作都是串行的 例如账户 A 转账户 B 账户 C 转账户 D 这两个转账操作现实世界里是可以并行的

随机推荐

  • maven本地仓库jar注册

    mvn install install file Dfile name 包名称 jar DgroupId groupId DartifactId artifactId Dversion version Dpackaging jar 例
  • 《网络建设与运维》大赛试题解析

    网络建设与运维 大赛试题解析资源 CSDN文库 https download csdn net download weixin 41687096 87799021
  • Spring 中AspectJ框架简介说明

    转自 Spring 中AspectJ框架简介说明 在以前的章节中 我们学习了使用代理类实现AOP Spring 2 0 以后 Spring 新增了对 AspectJ 的支持 所以笔者建议大家在Spring 框架中 尽量使用AspectJ方式
  • 2.4总线操作和定时

    文章目录 一 引子 二 介绍 1 总线周期 2 总线定时规范 三 同步定时方式 1 过程 2 特点 3 优缺点 优点 缺点 四 异步定时方式 1 介绍 2 三种方式 1 不互锁方式 2 半互锁方式 3 全互锁方式 3 优缺点 优点 缺点 五
  • 《操作系统》 实验1_unix——io参考

    任务1 在当前用户目录下创建数据文件student txt 文件的内部信息存储格式为Sname S Sdept Sage Ssex 即 姓名 学号 学院 年龄 性别 每行一条记录 输入不少于10条学生记录 其中包括学生本人记录 编写程序ta
  • vscode 输入 npm install 报错: node-sass@8.0.0 install: `node scripts/install.js`

    1 报错信息描述 报错的原因及解决方案 自身入的坑 第一种 看一下这里是否有中文目录 有的话有可能会报错 第二种 管理员身份运行vscode 第三种 node sass版本问题 解决版本问题方案 1 报错信息描述 当我们在vscode中输入
  • 【计算机视觉

    文章目录 一 Co Scale Conv attentional Image Transformer CoaT 二 Pyramid Vision Transformer v2 PVTv2 三 Class Attention in Image
  • android studio编程时出现的错误:Cannot get property 'XXXX' on extra properties extension as it does not exist

    用Android Studio中导入第三方库工程的时候出现的问题 Error 28 0 Cannot get property junitVersion on extra properties extension as it does no
  • springAop实现事务管理控制

    Aop简要概述 Aop面向切面编程 可以实现代码的解耦合 提高代码的复用性 1 切面 切面的意思通俗的意思就是切入的代码 比如开启事务方法的代码 提交事务的代码 2 切入点 需要切入代码的地方 比如待执行代码的前 或者后 3 连接点 满足规
  • keil的错误: Error: Encountered an improper argument 的2019.6.22最新解决方法

    keil的错误 Error Encountered an improper argument 的解决方法 什么都不要改动 最正确的办法是重新破解
  • QT信号与槽连接(松耦合)

    GUI用户界面中 当用户操作一个窗口部件时 需要其他窗口部件响应 传统方式经常使用callback 回调机制 来实现 所谓回调即事先将函数指针作为参数传递另一个函数 然后在函数处理过程中适当地方调用回调函数回调机制有两个缺陷 类型不安全 不
  • 传感器学习——旋转编码器

    旋转编码器是将旋转机械位移量转换为电器信号 对该信号进行处理后检测位置 速度等的传感器 旋转编码器可分为 增量式 编码器和 绝对值 式编码器 1 增量式编码器 旋转盘转动时 光敏二极管断续收到发光二极管发出的光 从而输出方波 增量式编码器通
  • cuda11.1和cuDNN v8.8.1的安装目录问题

    cuda的不同版本文件路径是不一致的 在cuda10 1中 配置cudnn的文件路径是 sudo cp cuda include cudnn h usr local cuda 10 1 include sudo cp P cuda lib6
  • ESLint 工具

    ESLint 可组装的 javaScript 和 JSX 检查工具 规范代码风格 官网 ESLint 插件化的 JavaScript 代码检测工具 ESLint中文文档 VSCode 自动格式化代码 ESLint安装 项目创建时 配置 ES
  • pscp无密传数据

    pscp 是 PuTTY 带的工具 可用作 Windows 上的 scp 替代 就在 PuTTY 的安装目录 加入 PATH 就可以敲命令用 无密上传 下载数据需要将公钥写入服务器 但是 PuTTY 用的公 私钥是 ppk 的 不同于 ss
  • BufferedImage 和 Graphics2D 画图,背景色透明

    File f new File D tag 20141204 chengxu business dossier business dossier web src main webapp upload 2017 08 07 C7A23630C
  • Element级联选择器Cascader使用(保存、回显)

    环境 版本 idea 2020 1 Element UI 2 13 2 vue 2 6 11 官方文档 https element eleme cn zh CN component cascader 业务场景 业务需求要给设备选择存放位置
  • c#连接读取mysql内容(报警无法连接处理方法)

    文章目录 一 Unable to connect to any of the specified MySQL hosts 二 Authentication to host 127 0 0 1 for user root using meth
  • csdn要考试了

    csdn要考试了 对于自己的是一次考验 第一次顺利过关 那第二次呢 还能如愿吗 人不是很多 竞争不是很激烈 我对自己有绝对的信心 因为我知道努力就会有收获 希望所有的河软的学生能进入csdn深造 能有一个好的机会 能够有一个好的前途 我希望
  • 并发编程系列之并发容器:ConcurrentHashMap

    前言 之前我们讲了线程 锁以及队列同步器等等一些并发相关底层的东西 当然Java开发者在开发中很少直接去使用之前所讲的 因为Java为了简化开发 为我们提供了一整套并发容器和框架 但是这些容器和框架都是建立在之前所讲的基础之上的 今天就让我