×

ConcurrentHashMap详解

前端技术网 前端技术网 发表于2024-01-27 11:29:11 浏览3291 评论0

抢沙发发表评论

一、一图了解ConcurrentHashMap底层原理

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

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

ConcurrentHashMap详解

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

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

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

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

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

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

ConcurrentHashMap详解

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

10、TreeNode,hash>0,为红黑树具体节点。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分别表示高位和低位的链表(高链,低链)。即,扩容时,会用n&h==0来判断元素应该放在新数组的i位置还是i+n位置。n:元素组长度;h:元素hash值;i:元素所在的原数组索引位;。这样就会出现有些元素会被挂在低位,有些元素会被挂在高位,从而达到打散元素的目的。

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

二、简单总结ConcurrentHashMap

在并发使用到HashMap的时候,往往不建议直接用HashMap,因为HashMap在并发写数据的时候容易因为rehash的过程产生环形链表的情况。所以在并发使用Map结构时,一般建议使用ConcurrentHashMap。

在JDK1.7中ConcurrentHashMap采用了数组+Segment+分段锁的方式实现。

从上面的结构我们可以了解到,ConcurrentHashMap定位一个元素的过程需要进行两次Hash操作。第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部。

JDK8中ConcurrentHashMap参考了JDK8 HashMap的实现,采用了数组+链表+红黑树的实现方式来设计,内部大量采用CAS操作。并发控制使⽤ synchronized和 CAS来操作。(JDK1.6以后对 synchronized锁做了很多优化)整个看起来就像是优化过且线程安全的 HashMap,虽然在 JDK1.8中还能看到 Segment的数据结构,但

是已经简化了属性,只是为了兼容旧版本;

JDK1.8的Nod节点中value和next都用volatile修饰,保证并发的可见性。

可以理解为,synchronized只锁定当前链表或红⿊⼆叉树的⾸节点,这样只要 hash不冲突,就不会产⽣并发,效率⼜提升 N倍。

Hashtable和 JDK1.8之前的 HashMap的底层数据结构类似都是采⽤数组+链表的形式,数组是 HashMap的主体,链表则是主要为了解决哈希冲突⽽存在的;

Hashtable(同⼀把锁):使⽤ synchronized来保证线程安全,效率⾮常低下。当⼀个线程访问同步⽅法时,其他线程也访问同步⽅法,可能会进⼊阻塞或轮询状态,如使⽤ put添加元素,另⼀个线程不能使⽤ put添加元素,也不能使⽤get,竞争会越来越激烈效率越低;

总结一下:

JavaGuide

三、HashMap、HashTable、ConcurrentHashMap的原理与区别

从类图中可以看出来在存储结构中ConcurrentHashMap比HashMap多出了一个类Segment。 ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一个可重入锁(ReentrantLock),在ConcurrentHashMap里扮演锁的角色;HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组。Segment的结构和HashMap类似,是一种数组和链表结构。一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素。当对HashEntry数组的数据进行修改时,必须首先获得与它对应的segment锁。

ConcurrentHashMap是使用了锁分段技术来保证线程安全的。

锁分段技术:首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

ConcurrentHashMap提供了与Hashtable和SynchronizedMap不同的锁机制。Hashtable中采用的锁机制是一次锁住整个hash表,从而在同一时刻只能由一个线程对其进行操作;而ConcurrentHashMap中则是一次锁住一个桶。

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

四、hashmap和concurrenthashmap的区别是什么

最大的区别就是ConcurrentHashMap是线程安全的,hashMap不是线程安全的。

基于哈希表的 Map接口的实现。此实现提供所有可选的映射操作,并允许使用 null值和 null键。(除了非同步和允许使用 null之外,HashMap类与Hashtable大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

此实现假定哈希函数将元素适当地分布在各桶之间,可为基本操作(get和 put)提供稳定的性能。迭代 collection视图所需的时间与 HashMap实例的“容量”(桶的数量)及其大小(键-值映射关系数)成比例。

重要参数

HashMap的实例有两个参数影响其性能:初始容量和加载因子。容量是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。

当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。在Java编程语言中,加载因子默认值为0.75,默认哈希表元为101。

END,本文到此结束,如果可以帮助到大家,还望关注本站哦!