concurrenthashmap继承自谁,concurrenthashmap的父类

http://www.itjxue.com  2023-01-04 19:58  来源:未知  点击次数: 

如何在java中使用ConcurrentHashMap

ConcurrentHashMapkey,value map = new ConcurrentHashMapkey,value ();

注意, key, value为类型

用法跟 HashMap一样的, 只是ConcurrentHashMap是并发集合, 线程安全的

一图了解ConcurrentHashMap底层原理

1、ConcurrentHashMap底层数据结构是一个数组table

2、table数组上挂着单向链表或红黑树

3、new ConcurrentHashMap();如果没有指定长度的话,默认是16,并且数组长度必须是2的n次幂,若自定义初始化的长度不是2的n次幂,那么在初始化数组时,会吧数组长度设置为大于自定义长度的最近的2的n次幂。(如:自定义长度为7,那么实际初始化数组后的长度为8)

4、底层是使用synchronized作为同步锁,并且锁的粒度是数组的具体索引位(有些人称之为分段锁)。

5、Node节点,hash0,当hash冲突时,会形成一个单向链表挂在数组上。

6、ForwardindNode,继承至Node,其hash=-1,表示当前索引位的数据已经被迁移到新数组上了

7、ReservationNode,继承至Node,其hash=-3,表示当前索引位被占用了(compute方法)

8、TreenBin,继承至Node,其hash=-2,代表当前索引下挂着一颗红黑树

9、lockState为ConcurrentHashMap底层自己实现的基于cas的读写锁,锁粒度是具体的某颗树。当向红黑树进行增,删操作时,首先会先上sync同步锁,然后自平衡的时候会上lockState写锁,当读红黑树的时候,会上lockState读锁,然后判断此时是否有线程正持有写锁,或是否有线程正在等待获取写锁,若有,则读线程会直接去读双向链表,而不是去读红黑树。

10、TreeNode,hash0,为红黑树具体节点。TreeBin的root代表红黑树的根节点,first代表双向链表的第一个节点

11、双向链表:构建红黑树时还维护了一个双向链表,其目的为:(1)当并发写读同一颗红黑树的时候,读线程判断到有线程正持有写锁,那么会跑去读取双向链表,避免因为并发写读导致读线程等待或阻塞。(2)当扩容的时候,会去遍历双向链表,重新计算节点hash,根据新的hash判断放在新数组的高位还是地位

1、触发扩容的规则:

1)添加完元素后,若判断到当前容器元素个数达到了扩容的阈值(数组长度*0.75),则会触发扩容

2)添加完元素后,所在的单向链表长度=8,则会转为红黑树,不过在转红黑树之前会判断数组长度是否小于64,若小于64则会触发扩容

2、table为扩容前的数组,nextTable为扩容中创建的新数组,迁移数据完毕后需要将nextTable赋值给table

3、扩容后的数组是元素组长度的2倍

4、ln,hn分别表示高位和低位的链表(高链,低链)。即,扩容时,会用nh==0来判断元素应该放在新数组的i位置还是i+n位置。n:元素组长度;h:元素hash值;i:元素所在的原数组索引位;。这样就会出现有些元素会被挂在低位,有些元素会被挂在高位,从而达到打散元素的目的。

5、红黑树扩容时,会遍历双向链表,并且计算nh==0来判断元素放在低位(lo)还是高位(ho),确定完元素的去处之后,会判断分别判断两个双向链表(lo,ho)的长度是否大于6,若大于6则将还是以一颗红黑树的结构挂在数组上,若=6的话,则转为单向链表,挂在数组上

HashMap HashTable和ConcurrentHashMap的区别

(条理上还需要整理,也是先说相同点,再说不同点)

HashMap是Hashtable的轻量级实现(非线程安全的实现),他们都完成了Map接口,主要区别在于HashMap允许空(null)键值(key),由于非线程安全,在只有一个线程访问的情况下,效率要高于Hashtable。

HashMap允许将null作为一个entry的key或者value,而Hashtable不允许。

HashMap把Hashtable的contains方法去掉了,改成containsvalue和containsKey。因为contains方法容易让人引起误解。

Hashtable继承自Dictionary类,而HashMap是Java1.2引进的Map interface的一个实现。

最大的不同是,Hashtable的方法是Synchronize的,而HashMap不是,在多个线程访问Hashtable时,不需要自己为它的方法实现同步,而HashMap 就必须为之提供外同步。

Hashtable和HashMap采用的hash/rehash算法都大概一样,所以性能不会有很大的差异。

就HashMap与HashTable主要从三方面来说。

一.历史原因:Hashtable是基于陈旧的Dictionary类的,HashMap是Java 1.2引进的Map接口的一个实现

二.同步性:Hashtable是线程安全的,也就是说是同步的,而HashMap是线程序不安全的,不是同步的

三.值:只有HashMap可以让你将空值作为一个表的条目的key或value

ConcurrentHashMap

HashMap是我们用得非常频繁的一个集合,但是它是线程不安全的。并且在多线程环境下,put操作是有可能产生死循环,不过在JDK1.8的版本中更换了数据插入的顺序,已经解决了这个问题。

为了解决该问题,提供了Hashtable和Collections.synchronizedMap(hashMap)两种解决方案,但是这两种方案都是对读写加锁,独占式。一个线程在读时其他线程必须等待,吞吐量较低,性能较为低下。而J.U.C给我们提供了高性能的线程安全HashMap:ConcurrentHashMap。

在1.8版本以前,ConcurrentHashMap采用分段锁的概念,使锁更加细化,但是1.8已经改变了这种思路,而是利用CAS+Synchronized来保证并发更新的安全,当然底层采用数组+链表+红黑树的存储结构。

HashMap 是最简单的,它不支持并发操作,下面这张图是 HashMap 的结构:

HashMap 里面是一个数组,然后数组中每个元素是一个单向链表。每个绿色的实体是嵌套类 Entry 的实例,Entry 包含四个属性:key, value, hash 值和用于单向链表的 next。

public HashMap(int initialCapacity, float loadFactor) 初始化方法的参数说明:

put 过程

get过程

ConcurrentHashMap 和 HashMap 思路是差不多的,但是因为它支持并发操作,所以要复杂一些。

整个 ConcurrentHashMap 由一个个 Segment 组成,Segment 代表”部分“或”一段“的意思,所以很多人都会将其描述为分段锁。简单的说,ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的。

再具体到每个 Segment 内部,其实每个 Segment 很像之前介绍的 HashMap,每次操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的。

初始化

public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) 初始化方法

举个简单的例子:

用 new ConcurrentHashMap() 无参构造函数进行初始化的,那么初始化完成后:

put过程

get过程

Java8 对 HashMap 进行了一些修改,最大的不同就是利用了红黑树,所以其由 数组+链表+红黑树 组成。

根据 Java7 HashMap 的介绍,我们知道,查找的时候,根据 hash 值我们能够快速定位到数组的具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度。

为了降低这部分的开销,在 Java8 中,当链表中的元素超过了 8 个以后,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度。

jdk7 中使用 Entry 来代表每个 HashMap 中的数据节点,jdk8 中使用 Node,基本没有区别,都是 key,value,hash 和 next 这四个属性,不过,Node 只能用于链表的情况,红黑树的情况需要使用 TreeNode。

我们根据数组元素中,第一个节点数据类型是 Node 还是 TreeNode 来判断该位置下是链表还是红黑树的。

put过程

和jdk7的put差不多

get 过程分析

Java7 中实现的 ConcurrentHashMap 还是比较复杂的,Java8 对 ConcurrentHashMap 进行了比较大的改动。可以参考 Java8 中 HashMap 相对于 Java7 HashMap 的改动,对于 ConcurrentHashMap,Java8 也引入了红黑树。

在1.8版本以前,ConcurrentHashMap采用分段锁的概念,使锁更加细化,但是1.8已经改变了这种思路,而是利用CAS+Synchronized来保证并发更新的安全,底层采用数组+链表+红黑树的存储结构。

ConcurrentHashMap通常只被看做并发效率更高的Map,用来替换其他线程安全的Map容器,比如Hashtable和Collections.synchronizedMap。线程安全的容器,特别是Map,很多情况下一个业务中涉及容器的操作有多个,即复合操作,而在并发执行时,线程安全的容器只能保证自身的数据不被破坏,和数据在多个线程间是可见的,但无法保证业务的行为是否正确。

ConcurrentHashMap总结:

案例2:业务操作的线程安全不能保证

案例3:多线程删除

12.7 对比Hashtable

Hashtable和ConcurrentHashMap的不同点:

Hashtable对get,put,remove都使用了同步操作,它的同步级别是正对Hashtable来进行同步的,也就是说如果有线程正在遍历集合,其他的线程就暂时不能使用该集合了,这样无疑就很容易对性能和吞吐量造成影响,从而形成单点。而ConcurrentHashMap则不同,它只对put,remove操作使用了同步操作,get操作并不影响。

Hashtable在遍历的时候,如果其他线程,包括本线程对Hashtable进行了put,remove等更新操作的话,就会抛出ConcurrentModificationException异常,但如果使用ConcurrentHashMap的话,就不用考虑这方面的问题了

concurrenthashmap线程安全吗

ConcurrentHashMap 是 Java 并发包中提供的一个线程安全且高效的 HashMap 实现,以弥补 HashMap 不适合在并发环境中操作使用的不足,本文就来分析下 ConcurrentHashMap 的实现原理,并对其实现原理进行分析!

一、摘要

在之前的集合文章中,我们了解到 HashMap 在多线程环境下操作可能会导致程序死循环的线上故障!

既然在多线程环境下不能使用 HashMap,那如果我们想在多线程环境下操作 map,该怎么操作呢?

想必阅读过小编之前写的《HashMap 在多线程环境下操作可能会导致程序死循环》一文的朋友们一定知道,其中有一个解决办法就是使用 java 并发包下的 ConcurrentHashMap 类!

今天呢,我们就一起来聊聊 ConcurrentHashMap 这个类!

二、简介

众所周知,在 Java 中,HashMap 是非线程安全的,如果想在多线程下安全的操作 map,主要有以下解决方法:

第一种方法,使用Hashtable线程安全类;

第二种方法,使用Collections.synchronizedMap方法,对方法进行加同步锁;

第三种方法,使用并发包中的ConcurrentHashMap类;

在之前的文章中,关于 Hashtable 类,我们也有所介绍,Hashtable 是一个线程安全的类,Hashtable 几乎所有的添加、删除、查询方法都加了synchronized同步锁!

相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞等待需要的锁被释放,在竞争激烈的多线程场景中性能就会非常差,所以 Hashtable 不推荐使用!

?

再来看看第二种方法,使用Collections.synchronizedMap方法,我们打开 JDK 源码,部分内容如下:

?

可以很清晰的看到,如果传入的是 HashMap 对象,其实也是对 HashMap 做的方法做了一层包装,里面使用对象锁来保证多线程场景下,操作安全,本质也是对 HashMap 进行全表锁!

使用Collections.synchronizedMap方法,在竞争激烈的多线程环境下性能依然也非常差,所以不推荐使用!

上面 2 种方法,由于都是对方法进行全表锁,所以在多线程环境下容易造成性能差的问题,因为** hashMap 是数组 + 链表的数据结构,如果我们把数组进行分割多段,对每一段分别设计一把同步锁,这样在多线程访问不同段的数据时,就不会存在锁竞争了,这样是不是可以有效的提高性能?**

再来看看第三种方法,使用并发包中的ConcurrentHashMap类!

ConcurrentHashMap 类所采用的正是分段锁的思想,将 HashMap 进行切割,把 HashMap 中的哈希数组切分成小数组,每个小数组有 n 个 HashEntry 组成,其中小数组继承自ReentrantLock(可重入锁),这个小数组名叫Segment, 如下图:

?

当然,JDK1.7 和 JDK1.8 对 ConcurrentHashMap 的实现有很大的不同!

JDK1.8 对 HashMap 做了改造,当冲突链表长度大于 8 时,会将链表转变成红黑树结构,上图是 ConcurrentHashMap 的整体结构,参考 JDK1.7!

我们再来看看 JDK1.8 中 ConcurrentHashMap 的整体结构,内容如下:

?

JDK1.8 中 ConcurrentHashMap 类取消了 Segment 分段锁,采用 CAS + synchronized 来保证并发安全,数据结构跟 jdk1.8 中 HashMap 结构类似,都是数组 + 链表(当链表长度大于 8 时,链表结构转为红黑二叉树)结构。

ConcurrentHashMap 中 synchronized 只锁定当前链表或红黑二叉树的首节点,只要节点 hash 不冲突,就不会产生并发,相比 JDK1.7 的 ConcurrentHashMap 效率又提升了 N 倍!

说了这么多,我们再一起来看看 ConcurrentHashMap 的源码实现。

三、JDK1.7 中的 ConcurrentHashMap

JDK 1.7 的 ConcurrentHashMap 采用了非常精妙的分段锁策略,打开源码,可以看到 ConcurrentHashMap 的主存是一个 Segment 数组。

?

我们再来看看 Segment 这个类,在 ConcurrentHashMap 中它是一个静态内部类,内部结构跟 HashMap 差不多,源码如下:

?

存放元素的 HashEntry,也是一个静态内部类,源码如下:

?

HashEntry和HashMap中的 Entry非常类似,唯一的区别就是其中的核心数据如value ,以及next都使用了volatile关键字修饰,保证了多线程环境下数据获取时的可见性!

从类的定义上可以看到,Segment 这个静态内部类继承了ReentrantLock类,ReentrantLock是一个可重入锁,如果了解过多线程的朋友们,对它一定不陌生。

ReentrantLock和synchronized都可以实现对线程进行加锁,不同点是:ReentrantLock可以指定锁是公平锁还是非公平锁,操作上也更加灵活,关于此类,具体在以后的多线程篇幅中会单独介绍。

因为ConcurrentHashMap的大体存储结构和HashMap类似,所以就不对每个方法进行单独分析介绍了,关于HashMap的分析,有兴趣的朋友可以参阅小编之前写的《深入分析 HashMap》一文。

ConcurrentHashMap 在存储方面是一个 Segment 数组,一个 Segment 就是一个子哈希表,Segment 里维护了一个 HashEntry 数组,其中 Segment 继承自 ReentrantLock,并发环境下,对于不同的 Segment 数据进行操作是不用考虑锁竞争的,因此不会像 Hashtable 那样不管是添加、删除、查询操作都需要同步处理。

理论上 ConcurrentHashMap 支持 concurrentLevel(通过 Segment 数组长度计算得来) 个线程并发操作,每当一个线程独占一把锁访问 Segment 时,不会影响到其他的 Segment 操作,效率大大提升!

上面介绍完了对象属性,我们继续来看看 ConcurrentHashMap 的构造方法,源码如下:

?

this调用对应的构造方法,源码如下:

?

从源码上可以看出,ConcurrentHashMap 初始化方法有三个参数,initialCapacity(初始化容量)为 16、loadFactor(负载因子)为 0.75、concurrentLevel(并发等级)为 16,如果不指定则会使用默认值。

其中,值得注意的是 concurrentLevel 这个参数,虽然 Segment 数组大小 ssize 是由 concurrentLevel 来决定的,但是却不一定等于 concurrentLevel,ssize 通过位移动运算,一定是大于或者等于 concurrentLevel 的最小的 2 的次幂!

通过计算可以看出,按默认的 initialCapacity 初始容量为 16,concurrentLevel 并发等级为 16,理论上就允许 16 个线程并发执行,并且每一个线程独占一把锁访问 Segment,不影响其它的 Segment 操作!

从之前的文章中,我们了解到 HashMap 在多线程环境下操作可能会导致程序死循环,仔细想想你会发现,造成这个问题无非是 put 和扩容阶段发生的!

那么这样我们就可以从 put 方法下手了,来看看 ConcurrentHashMap 是怎么操作的?

3.1、put 操作

ConcurrentHashMap 的 put 方法,源码如下:

?

从源码可以看出,这部分的 put 操作主要分两步:

定位 Segment 并确保定位的 Segment 已初始化;

调用 Segment 的 put 方法;

真正插入元素的 put 方法,源码如下:

?

从源码可以看出,真正的 put 操作主要分以下几步:

第一步,尝试获取对象锁,如果获取到返回 true,否则执行scanAndLockForPut方法,这个方法也是尝试获取对象锁;

第二步,获取到锁之后,类似 hashMap 的 put 方法,通过 key 计算所在 HashEntry 数组的下标;

第三步,获取到数组下标之后遍历链表内容,通过 key 和 hash 值判断是否 key 已存在,如果已经存在,通过标识符判断是否覆盖,默认覆盖;

第四步,如果不存在,采用头插法插入到 HashEntry 对象中;

第五步,最后操作完整之后,释放对象锁;

我们再来看看,上面提到的scanAndLockForPut这个方法,源码如下:

?

scanAndLockForPut这个方法,操作也是分以下几步:

当前线程尝试去获得锁,查找 key 是否已经存在,如果不存在,就创建一个 HashEntry 对象;

如果重试次数大于最大次数,就调用lock()方法获取对象锁,如果依然没有获取到,当前线程就阻塞,直到获取之后退出循环;

在这个过程中,key 可能被别的线程给插入,所以在第 5 步中,如果 HashEntry 存储内容发生变化,重置重试次数;

通过scanAndLockForPut()方法,当前线程就可以在即使获取不到segment锁的情况下,完成需要添加节点的实例化工作,当获取锁后,就可以直接将该节点插入链表即可。

这个方法还实现了类似于自旋锁的功能,循环式的判断对象锁是否能够被成功获取,直到获取到锁才会退出循环,防止执行 put 操作的线程频繁阻塞,这些优化都提升了 put 操作的性能。

3.2、get 操作

get 方法就比较简单了,因为不涉及增、删、改操作,所以不存在并发故障问题,源码如下:

?

由于 HashEntry 涉及到的共享变量都使用 volatile 修饰,volatile 可以保证内存可见性,所以不会读取到过期数据。

3.3、remove 操作

remove 操作和 put 方法差不多,都需要获取对象锁才能操作,通过 key 找到元素所在的 Segment 对象然后移除,源码如下:

?

与 get 方法类似,先获取 Segment 数组所在的 Segment 对象,然后通过 Segment 对象去移除元素,源码如下:

?

先获取对象锁,如果获取到之后执行移除操作,之后的操作类似 hashMap 的移除方法,步骤如下:

先获取对象锁;

计算 key 的 hash 值在 HashEntry[]中的角标;

根据 index 角标获取 HashEntry 对象;

循环遍历 HashEntry 对象,HashEntry 为单向链表结构;

通过 key 和 hash 判断 key 是否存在,如果存在,就移除元素,并将需要移除的元素节点的下一个,向上移;

最后就是释放对象锁,以便其他线程使用;

四、JDK1.8 中的 ConcurrentHashMap

虽然 JDK1.7 中的 ConcurrentHashMap 解决了 HashMap 并发的安全性,但是当冲突的链表过长时,在查询遍历的时候依然很慢!

在 JDK1.8 中,HashMap 引入了红黑二叉树设计,当冲突的链表长度大于 8 时,会将链表转化成红黑二叉树结构,红黑二叉树又被称为平衡二叉树,在查询效率方面,又大大的提高了不少。

?

因为 HashMap 并不支持在多线程环境下使用, JDK1.8 中的 ConcurrentHashMap 和往期 JDK 中的 ConcurrentHashMa 一样支持并发操作,整体结构和 JDK1.8 中的 HashMap 类似,相比 JDK1.7 中的 ConcurrentHashMap, 它抛弃了原有的 Segment 分段锁实现,采用了 CAS + synchronized 来保证并发的安全性。

JDK1.8 中的 ConcurrentHashMap 对节点Node类中的共享变量,和 JDK1.7 一样,使用volatile关键字,保证多线程操作时,变量的可见行!

?

其他的细节,与 JDK1.8 中的 HashMap 类似,我们来具体看看 put 方法!

4.1、put 操作

打开 JDK1.8 中的 ConcurrentHashMap 中的 put 方法,源码如下:

?

当进行 put 操作时,流程大概可以分如下几个步骤:

首先会判断 key、value 是否为空,如果为空就抛异常!

接着会判断容器数组是否为空,如果为空就初始化数组;

进一步判断,要插入的元素f,在当前数组下标是否第一次插入,如果是就通过 CAS 方式插入;

在接着判断f.hash == -1是否成立,如果成立,说明当前f是ForwardingNode节点,表示有其它线程正在扩容,则一起进行扩容操作;

其他的情况,就是把新的Node节点按链表或红黑树的方式插入到合适的位置;

节点插入完成之后,接着判断链表长度是否超过8,如果超过8个,就将链表转化为红黑树结构;

最后,插入完成之后,进行扩容判断;

put 操作大致的流程,就是这样的,可以看的出,复杂程度比 JDK1.7 上了一个台阶。

4.1.1、initTable 初始化数组

我们再来看看源码中的第 3 步 initTable()方法,如果数组为空就初始化数组,源码如下:

?

sizeCtl 是一个对象属性,使用了 volatile 关键字修饰保证并发的可见性,默认为 0,当第一次执行 put 操作时,通过Unsafe.compareAndSwapInt()方法,俗称CAS,将 sizeCtl修改为 -1,有且只有一个线程能够修改成功,接着执行 table 初始化任务。

如果别的线程发现sizeCtl0,意味着有另外的线程执行 CAS 操作成功,当前线程通过执行Thread.yield()让出 CPU 时间片等待 table 初始化完成。

4.1.2、helpTransfer 帮组扩容

我们继续来看看 put 方法中第 5 步helpTransfer()方法,如果f.hash == -1成立,说明当前f是ForwardingNode节点,意味有其它线程正在扩容,则一起进行扩容操作,源码如下:

?

这个过程,操作步骤如下:

第 1 步,对 table、node 节点、node 节点的 nextTable,进行数据校验;

第 2 步,根据数组的 length 得到一个标识符号;

第 3 步,进一步校验 nextTab、tab、sizeCtl 值,如果 nextTab 没有被并发修改并且 tab 也没有被并发修改,同时 sizeCtl 0,说明还在扩容;

第 4 步,对 sizeCtl 参数值进行分析判断,如果不满足任何一个判断,将sizeCtl + 1, 增加了一个线程帮助其扩容;

4.1.3、addCount 扩容判断

我们再来看看源码中的第 9 步 addCount()方法,插入完成之后,扩容判断,源码如下:

?

这个过程,操作步骤如下:

第 1 步,利用 CAS 将方法更新 baseCount 的值

第 2 步,检查是否需要扩容,默认 check = 1,需要检查;

第 3 步,如果满足扩容条件,判断当前是否正在扩容,如果是正在扩容就一起扩容;

第 4 步,如果不在扩容,将 sizeCtl 更新为负数,并进行扩容处理;

put 的流程基本分析完了,可以从中发现,里面大量的使用了CAS方法,CAS 表示比较与替换,里面有 3 个参数,分别是目标内存地址、旧值、新值,每次判断的时候,会将旧值与目标内存地址中的值进行比较,如果相等,就将新值更新到内存地址里,如果不相等,就继续循环,直到操作成功为止!

虽然使用的了CAS这种乐观锁方法,但是里面的细节设计的很复杂,阅读比较费神,有兴趣的朋友们可以自己研究一下。

4.2、get 操作

get 方法操作就比较简单了,因为不涉及并发操作,直接查询就可以了,源码如下:

?

从源码中可以看出,步骤如下:

第 1 步,判断数组是否为空,通过 key 定位到数组下标是否为空;

第 2 步,判断 node 节点第一个元素是不是要找到,如果是直接返回;

第 3 步,如果是红黑树结构,就从红黑树里面查询;

第 4 步,如果是链表结构,循环遍历判断;

4.3、reomve 操作

reomve 方法操作和 put 类似,只是方向是反的,源码如下:

?

replaceNode 方法,源码如下:

?

从源码中可以看出,步骤如下:

第 1 步,循环遍历数组,接着校验参数;

第 2 步,判断是否有别的线程正在扩容,如果是一起扩容;

第 3 步,用 synchronized 同步锁,保证并发时元素移除安全;

第 4 步,因为 check= -1,所以不会进行扩容操作,利用 CAS 操作修改 baseCount 值;

五、总结

虽然 HashMap 在多线程环境下操作不安全,但是在 java.util.concurrent 包下,java 为我们提供了 ConcurrentHashMap 类,保证在多线程下 HashMap 操作安全!

在 JDK1.7 中,ConcurrentHashMap 采用了分段锁策略,将一个 HashMap 切割成 Segment 数组,其中 Segment 可以看成一个 HashMap, 不同点是 Segment 继承自 ReentrantLock,在操作的时候给 Segment 赋予了一个对象锁,从而保证多线程环境下并发操作安全。

但是 JDK1.7 中,HashMap 容易因为冲突链表过长,造成查询效率低,所以在 JDK1.8 中,HashMap 引入了红黑树特性,当冲突链表长度大于 8 时,会将链表转化成红黑二叉树结构。

在 JDK1.8 中,与此对应的 ConcurrentHashMap 也是采用了与 HashMap 类似的存储结构,但是 JDK1.8 中 ConcurrentHashMap 并没有采用分段锁的策略,而是在元素的节点上采用 CAS + synchronized 操作来保证并发的安全性,源码的实现比 JDK1.7 要复杂的多。

(责任编辑:IT教学网)

更多

相关站内动态文章

推荐站内动态文章