ConcurrentHashMap1.7的实现原理与使用

ConcurrentHashMap的实现原理(JDK1.7和JDK1.8) 哈希表 1.介绍 哈希表就是一种以 键-值(key-indexed) 存储数据的结构,我们只要输入待查找的值即key,即可查找到其对应的值。 哈希的思路很简单,如果所有的键都是整数,那么就可以使用一个简单的无序数组来实现:将键作为索引,值即为其对应的值,这样就可以快速访问任意键的值。这是对于简单的键的情况,我们将其扩展到可以处理更加复杂的类型的键。 2.链式哈希表 链式哈希表从根本上说是由一组... 阅读详情

1. 为什么要使用ConcurrentHashMap?

  ConcurrentHashMap是线程安全且高效的HashMap。在并发编程中,如果只是使用HashMap的话可能导致程序死循环。而使用线程安全的HashTable效率又非常低,所以便有了ConcurrentHashMap。

1.1 线程不安全的HashMap

  在多线程环境下,使用HashMap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap。下面一个是一个会引起死循环的示例。

 		final HashMap<String, String> map = new HashMap<>(2);
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                new Thread(() -> {
                    map.put(UUID.randomUUID().toString(), "");
                }, "ftf" + i).start();
            }
        }, "ftf");
        thread.start();
        thread.join();

  HashMap在并发执行put操作时会引起死循环,是因为多线程导致HashMap的Entry链表形成环形数据结构,一旦形成环形数据结构,Entry的next节点永远不为空,就会产生死循环获取Entry。

1.2 效率低下的HashTable

  HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable效率非常低下。因为当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同步方法时,会进入阻塞或轮询状态。比如线程1使用put进行元素添加,线程2不但不能使用put方法添加元素,也不能使用get方法来获取元素,所以竞争越激烈效率越低。

1.3 ConcurrentHashMap的锁分段技术可有效提升并发访问率

  HashTable容器在竞争激烈的并发环境下表现出效率低下的原因是所访问HashTable的线程必须竞争同一把锁,假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数段的数据时,线程间就不会存在锁竞争,从而可以有效提高并发访问率,这就是ConcurrentHashMap所使用的锁分段技术。首先将数据分成一段一段地存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。


2. ConcurrentHashMap的结构

  ConcurrentHashMap是由Segment数组结构和HashEntry数字结构组成。Segment是一种可重入锁(ReentrantLock),在ConcurrentHashMap里扮演锁的角色;HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组。Segment的结构和HashEentry类似,是一种数组和链表的结构。一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得与他对应的Segment锁。
下图是ConcurrentHashMap的结构图以便理解
ConcurrentHashMap的结构图.jpg

3. ConcurrentHashMap的初始化

  ConcurrentHashMap初始化方法是通过initialCapacity、loadFactor个concurrencyLevel等几个参数来初始化segment数组、段偏移量segmentShift、段掩码segmentMask和每个segment里的HashEntry数组来实现。

3.1 初始化segments数组

  首先我们看一下初始化源码

	if (conrrencyLevel > MAX_SEGMENTS)
		concurrencyLevel = MAX_SEGMENTS;
	int sshift = 0;
	int ssize = 0;
	while(ssize < concurrencyLevel ) {
		++sshift;
		ssize <<= 1; 
	}
	segmentShift = 32 - sshift;
	segmentMask = ssize - 1;
	this.segments = Segment.newArrary(ssize);

  由上面代码可以看出,segments数组的长度ssize是通过concurrencyLevel 计算得出的。为了能按位与的散列算法来定位segments数组的索引,必须保住segments数组的长度是2的N次方(power-of-two size),所以必须计算出一个大于或等于concurrencyLevel的最小的2的N次方的值来作为segments数组的长度。

3.2 初始化 segmentShift 和 segmentMask

  这两个全局变量需要在定位segment时的散列算法里使用,sshift等于ssize从1向左移位的次数,在默认情况下concurrencyLevel等于16,1需要向左移位4次,所以sshift等于4。segmentShift用于定位参数散列运算的位数,segmentShift等于32-sshift,所以等于28,这里之所以用32是因为ConcurrentHashMap里的hash()方法输出的最大数是32位的。segmentMask是散列运算的掩码,等于ssize-1,即15,掩码的二进制各个位的值都是1。因为ssize的最大长度是65536,所以segmentShift最大值是16,segmentMask最大值是65535,对应的二进制是16位,每个位都是1。

3.3 初始化每个segment

  输入参数initialCapacity是ConcurrentHashMap的初始化容量,loadFactor是每个segment的负载因子,在构造方法里通过这两个参数来初始化数组中的每个segment。

	if (initialCapacity > MAXIMUM_CAPACITY)
		initialCapacity  = MAXIMUM_CAPACITY;
	int c = initialCapacity / ssize;
	if (c * ssize < initialCapacity)
		++c;
	int cap = 1;
	while(cap < c)
		cap <<= 1;
	
	for (int i = 0; i < this.segments.length; ++i) {
		this.segments[i] = new Segment<K,V>(cap, loadFator);
	}
	

  上面代码中的变量cap就是segment里的HashEntry数组的长度,他等于initialCapacity 除以ssize的倍数c,如果c大于1,就会取大于等于c的2的N次方的值,所以cap不是1,就是2的N次方。segment的容量threshold = (int) cap * loadFactor,默认情况下initialCapacity 等于16,loadFactor等于0.75,通过运算cap等于1,threshold等于0。

4. 定位Segment

  既然ConcurrentHashMap使用分段锁Segment来保护不同段的数据,那么在插入和获取元素的时候,必须先通过散列算法定位到Segment。可以看到ConcurrentHashMap会首先使用 hash(int h) 算法对元素的hashCode进行一次再散列。

private static int hash(int h) {
	h += (h << 15) ^ 0xfffffcd7d;
	h ^= (h >>> 10);
	h += (h << 3);
	h ^= (h >>> 6);
	h += (h << 2) + (h << 14);
	return h ^ (h >>> 16);
}

  之所以进行再散列,目的是减少散列冲突,使元素能够均匀地分布在不同的Segment上,从而提高容器的存取效率。假如散列的质量差到极点,那么所有的元素都在一个Segment中,不仅存取元素缓慢,分段锁也会失去意义。

5. ConcurrentHashMap的操作

5.1 get操作

  Segment的get操作实现非常简单和高效。先经过一次再散列,然后使用这个散列值通过散列运算定位到Segment,在通过散列算法定位到元素。

public V get(Object key) {
	int hash = hash(key.hashCode());
	return segmentFor(hash).get(key, hash);
}

  get操作的高效之处在于整个get过程不需要加锁,除非读到的值是空才会加锁重读。我们知道HashTable容器的get方法是需要加锁的,那么ConcurrentHashMap的get操作是如何做到不加锁的呢?原因是他的get方法里将要使用的共享变量都定义成volatile类型,如用于统计当前Segment大小的count字段和用于存储值的HashEntry的value。定义成volatile的变量,能够在线程间保持可见性,能够被多线程同时读,并且保证不会读到过期的值,但是只能被单线程写,在get操作里只需要读不需要写共享变量count和value,所以可以不用加锁。之所以不会读到过期的值,是因为根据JMM的happens-before原则,对volatile字段的写入操作先于读操作,即使两个线程同时修改和获取volatile变量,get操作也能拿到最新的值。

5.2 put操作

  由于put方法里需要对共享变量进行写入操作,所以为了线程安全,在操作共享变量时必须加锁。put方法首先定位到Segment,然后在Segment里面进行插入操作。插入操作需要经历两个步骤,第一步判断是否需要对Segmen里的HashEntry数组进行扩容,第二步定位添加元素的位置,然后将其放在HashEntry数组里。

  1. 是否需要扩容
      在插入元素前会先判断Segment里的HashEntry数组是否超过容量,如果超过阈值,则对数组进行扩容。值得一提的是,Segment的扩容判断比HashMap更恰当,因为HashMap是在插入元素后判断元素是否已经到达容量,如果达到了就进行扩容,但是很可能扩容之后没有新元素插入,这时HashMap就进行了一次无效的扩容。
  2. 如何扩容
      在扩容的时候,首先会创建一个容量时原来容量两倍的数组,然后将原数组里的元素进行再散列后插入到新数组里,为了高效,ConcurrentHashMap不会对整个容器进行扩容,而只对某个segment进行扩容。
5.3 size操作

  如果要统计整个ConcurrentHashMap里元素的大小,就必须统计所有Segment里元素的大小后求和。Segment里的全局变量count是一个volatile变量,那么在多线程场景下,是不是直接把所有Segment的count相加就可以得到整个ConcurrentHashMap大小了呢?不是的,虽然相加时可以获取每个Segment的count的最新值,但是可能累加钱使用的count发生了变化,那么统计的结果就不准了。所以,最安全的做法就是在统计size的时候把所有Segment的put、remove和clean方法全部锁住,但是这种做法显然非常低效。
  因为在累加count操作的过程中,之前累加过的count发生变化的几率非常小,所以ConcurrentHashMap的做法是先尝试2次通过不锁住Segment的方式来统计各个Segment大小,如果统计过程中容器的count发生了变化,则再采用加锁的方式来统计所有Segment的大小。
  那么ConcurrentHashMap是如何判断在统计的时候容器是否发生了变化呢?使用modCount变量,在put、remove和clean方法里操作元素前都会将变量modCount进行加1,那么在统计size前后比较modCount是否发生变化,从而得知容器的大小是否发生变化。


ConcurrentHashMap:使用方法和底层原理详解 ConcurrentHashMap是Java集合框架中的一个线程安全的哈希表实现,它提供了比Hashtable和同步的HashMap更高的并发性能。ConcurrentHashMap在JDK 1.5中引入,通过采用分段锁的方式实现了高效的并发访问。它可以在保证线程安全的同时,提供较高的并发性能,因此是多线程环境下使用频率较高的数据结构之一。 阅读详情

相关推荐

ConcurrentHashMap的使用介绍和底层原理解析和开源框架的使用实例

ConcurrentHashMap是JDK1.5提供的线程安全的HashMap,它允许多个线程并发访问哈希表,并发修改map中的数据而不会产生死锁。ConcurrentHashMap适用于高并发的环境下,可以替代synchronized实现的同步HashMap。ConcurrentHashMap的并发度很高,吞吐量也很高。ConcurrentHashMap通过锁分段技术,实现高度的并发访问,大大提高了HashMap的吞吐量,是高并发环境下一个很好的选择。

m0_71777195的博客 1245

ConcurrentHashMap用法详解

ConcurrentHashMap用法详解

jpfice的博客 2116

一文就懂ConcurrentHashMap实现原理

哈希表就是一种以键-值(key -indexed) 存储数据的结构,我们只要输入待查 找的值即 key,即可查找到其对应的值。哈希表是一种非常高效的数据结构 ,设计优良的哈希函数可以使其上的增删改查操作达到O(1)级别。Java为我们提供了一个现成的哈希结构 ,那就是HashMap类 ,在前面的文章中我曾经介绍过HashMap类 ,知道它的所有方法都未进行同步 ,因此在多线程环境中是不安全的。

流楚丶格念的博客 2526

ConcurrentHashMap

1.ConcurrentHashMap底层实现 1.1 JDK1.7 底层数据结构:Segments数组+HashEntry数组+链表,采用分段锁保证安全性 一个ConcurrentHashMap中有一个Segments数组,一个Segments中存储一个HashEntry数组,每个HashEntry是一个链表结构的元素。 segment继承自ReentrantLock锁。 首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一段数据时,其他段的数据也能被其他线程访问,实现了真

zqmk531933288的博客 2608

AMEsim不连续处理机制:仿真精度与效率的平衡艺术

本文深入解析AMEsim不连续处理机制在系统仿真中的应用,探讨如何平衡仿真精度与效率。通过液压系统仿真案例,详细介绍了事件检测算法、变步长求解器自适应策略及典型场景优化方法,帮助工程师有效应对仿真中的不连续性问题,提升仿真效率和准确性。

v4w5x6的博客 440

生信人填坑记 orthofinder的安装和基础使用

其中常用的有两个结果,第一个是Orthogroups文件夹下的Orthogroups.GeneCount.tsv,这个文件里保存着每个家族在每个物种里的成员数量。(写到这儿饭点到了,吃饭要紧…在国内使用conda的同学我相信很多人都会遇到-|- -|- -|- -|- -|- -|- -|- -|- 一直转圈。软件的是由很简单,将你要跑的物种的蛋白质文件放在work/data目录下,文件名都用物种缩写,后缀统一就行。当然,很多软件都会自带一个这样的文件,conda装不了的时候可以去软件官网溜达溜达。

qq_43337286的博客 5381

认真学习jdk1.7下ConcurrentHashMap的实现原理

HashMap无论是 1.7 还是 1.8 其实都能看出 JDK 没有对它做任何的同步操作,所以并发会出问题,甚至出现死循环导致系统不可用。这个问题就交给ConcurrentHashMap。 ConcurrentHashMap是一个 在juc包下的 map, 线程安全。 在jdk.1.8 之前采用数组+ 链表的结构 并且采用`分段锁机制` 来保证线程安全,而jdk1.8 改成了 数组+ 链表+ 红黑树,线程安全方面也改成了 `cas+ synchronized` 来保证线程安全。 **Concurre

小小默:进无止境 1479

因果强化学习:优化决策策略的因果方法

1. 背景介绍 1.1 传统强化学习的局限性 强化学习(Reinforcement Learning, RL)是一种通过与环境交互来学习最优决策策略的方法。然而,传统强化学习方法在面对复杂、动态和不确定的现实世界问题时,往往存在以下局限性: 数据效率低:传统强化学习方法

AI天才研究院 657

Java ConcurrentHashMap底层实现原理(JDK1.7&1.8)【总结】

为了彻底搞清楚ConcurrentHashMap的实现机制,我会先从它的底层数据实现:哈希表谈起 一、哈希表:原链接 1、介绍:哈希表就是一种以键-值(key-value)存储数据的结构,我们只要输入带查找的值即key,即可查找到其对应的值。 哈希表的思路很简单,如果所有的键都是整数,那么久可以使用一个简单的无序数组来实现:将键作为索引,值即为其对应的值,这样就可以快速访问任意键的值,这是对于简单的键的情况,我们将其扩展到可以处理更加复杂的类型的键。 2、链式哈希表:链式哈希表从根本上说是由一组链表构成。每

单枪匹码 2996

在isaacsim仿真系统进行遥操作(设备是quest3):怎么让遥操作设备(quest3)在Isaac sim系统中控制机械臂并把其中的参数传输出来??

我长期专注 Python 爬虫工程化实战,主理专栏 👉 《Python爬虫实战》:从采集策略到反爬对抗,从数据清洗到分布式调度,持续输出可复用的方法论与可落地案例。内容主打一个“能跑、能用、能扩展”,让数据价值真正做到——抓得到、洗得净、用得上。

**My Coding Family** 939

通用抓取算法AnyGrasp(Graspnet)——本地部署并测试自定义输入数据

背景:GraspNet在数据采集方面,首先将相机固定到机械臂末端,对同一个场景拍摄256个不同的角度,同时场景中的每个物体都会提前采集好3D模型,之后将其投射到仿真环境中,在仿真环境中,对抓取位姿进行打标签,如此完成数据的采集。在运行官方的测试示例时,使用的是路径下的输入数据,可以分析下。color.png(输入):rgb图像(输出):输出的生成抓取位姿的图像depth.png(输入):和当前rgb图像对应的深度图像(保存格式是单通道uint16)meta.mat。

boss-dog 1327

ConcurrentHashMap 的实现原理 (JDK1.7 和 JDK1.8)

HashMap、CurrentHashMap 的实现原理基本都是 BAT 面试必考内容,阿里 P8 架构师谈:深入探讨 HashMap 的底层结构、原理、扩容机制深入谈过 hashmap 的实现原理以及在 JDK 1.8 的实现区别,今天主要谈 CurrentHashMap 的实现原理,以及在 JDK1.7 和 1.8 的区别。内容目录:1. 哈希表 2.ConcurrentHashMap 与 H...

程序员世杰 3382

AES-128-CMAC:背景、汽车应用、CAPL 实现与开源工具验证

本文详解车载诊断中AES-128-CMAC算法的原理与应用,涵盖其在UDS安全访问、刷写鉴权、SecOC认证及密钥派生中的核心作用。结合真实汽车电子场景,提供纯CAPL实现代码与测试脚本,并通过pycryptodome进行逐字节验证,确保算法正确性。全程基于CANoe实测数据,可直接用于HIL环境复现,助力开发者高效落地汽车网络安全方案。

蚂蚁小兵 113

ConcurrentHashMap底层实现原理((JDK1.7&1.8))

目录前言ConcurrentHashMap (JDK1.7的实现)初始化put操作get操作size操作ConcurrentHashMap (JDK1.8的实现)NodeTreeNodeTreeBinput操作get操作size操作总结与思考 前言 我们都知道HashMap在多线程情况下,在put的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是rehash,这个会重新将原...

CoderZS的博客 1万+

反应动力学仿真软件:Kintecus_(4).用户界面及操作指南

在 Kintecus 中,反应方程的定义非常灵活。您可以定义简单的一阶反应、复杂的多步反应以及不同类型的反应机制。

kkchenjj的博客 1182

Megatron-LM从头训练大语言模型:并行策略图示【Tensor并行、Pipeline并行、Data并行、Sequence并行、virtual_pipeline并行、Context并行】

节点包含的层每GPU的参数内存每GPU的激活内存节点0Stage 0层1-1212层参数/2 (TP切分)8k序列激活 (CP切分)节点1Stage 1层13-2412层参数/2 (TP切分)8k序列激活 (CP切分)节点2Stage 2层25-3612层参数/2 (TP切分)8k序列激活 (CP切分)节点3Stage 3层37-4812层参数/2 (TP切分)8k序列激活 (CP切分)负载均衡:每个节点处理相同数量的层(12层)内存优化。

u013250861的博客 285

高并发编程系列:ConcurrentHashMap的实现原理(JDK1.7和JDK1.8)

HashMap、CurrentHashMap 的实现原理基本都是BAT面试必考内容,阿里P8架构师谈:深入探讨HashMap的底层结构、原理、扩容机制深入谈过hashmap的实现原理以及在JDK 1.8的实现区别,今天主要谈CurrentHashMap的实现原理,以及在JDK1.7和1.8的区别。 内容目录: 1.哈希表 2.ConcurrentHashMap与HashMap、HashTa...

【小石头的茅坑】 71万+

迈瑞BS系列全自动生化分析仪LIS协议接口手册(内含HL7和ASTM)

迈瑞BS系列全自动生化分析仪LIS协议接口手册,内含HL7和ASTM)

currenthashmap实现原理_ConcurrentHashMap的实现原理(JDK1.7和JDK1.8)

哈希表1.介绍哈希表就是一种以 键-值(key-indexed) 存储数据的结构,我们只要输入待查找的值即key,即可查找到其对应的值。哈希的思路很简单,如果所有的键都是整数,那么就可以使用一个简单的无序数组来实现:将键作为索引,值即为其对应的值,这样就可以快速访问任意键的值。这是对于简单的键的情况,我们将其扩展到可以处理更加复杂的类型的键。2.链式哈希表链式哈希表从根本上说是由一组链表构成。每个...

weixin_39835991的博客 537

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

ConcurrentHashMap底层实现原理(JDK1.7 & 1.8)

ConcurrentHashMap底层实现原理(JDK1.7 & 1.8) 前言 我们都知道HashMap在多线程情况下,在put的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容重新hash到新的扩容数组中,在多线程的环境下,存在同时其他的元素也在进行put操作,如果hash值相同,可能出现同时在同一数组下用链表表示,造成闭环,导致在get时会出现死循环,所以HashMap是线程不安全的。 我们来了解另一个键值存储集合HashTable

jiohfgj的博客 604

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

上一篇: Java中锁的介绍
下一篇: Rocket MQ 系列一 - 入门
geek-wmy
博客等级 码龄9年 322粉丝 126原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值