ConcurrentHashMap原理详解

什么是ConcurrentHashMap

ConcurrentHashMap和HashMap一样,是一个存放键值对的容器。使用hash算法来获取值的地址,因此时间复杂度是O(1)。查询非常快。
同时,ConcurrentHashMap是线程安全的HashMap。专门用于多线程环境。

ConcurrentHashMap和HashMap以及Hashtable的区别

2.1 HashMap
HashMap是线程不安全的,因为HashMap中操作都没有加锁,因此在多线程环境下会导致数据覆盖之类的问题,所以,在多线程中使用HashMap是会抛出异常的。

2.2 HashTable
HashTable是线程安全的,但是HashTable只是单纯的在put()方法上加上synchronized。保证插入时阻塞其他线程的插入操作。虽然安全,但因为设计简单,所以性能低下。

2.3 ConcurrentHashMap
ConcurrentHashMap是线程安全的,ConcurrentHashMap并非锁住整个方法,而是通过原子操作和局部加锁的方法保证了多线程的线程安全,且尽可能减少了性能损耗。
由此可见,HashTable可真是一无是处…

1.7和1.8的ConcurrentHashMap区别

JDK 1.7给Segment添加ReentrantLock锁来实现线程安全(分段锁)
JDK 1.8通过CAS或者synchronized来实现线程安全 (通过对指定的 Node 节点加锁)

线程不安全的HashMap(在多线程环境下,使用HashMap 进行put操作会引起死循环,导致CPU利用率接近100%)
效率低下的HashTable(HashTable 容器使用synchronized 来保证线程安全,但在线程竞争激烈的情况下HashTable 的效率非常低下)
ConcurrentHashMap 的锁分段技术可有效提升并发访问率。
HashMap不是线程安全的;Hashtable线程安全,但效率低,因为是Hashtable是使⽤synchronized的,所有线程竞争同⼀把锁;⽽ConcurrentHashMap不仅线程安全⽽且效率高,因为它包含⼀个segment数组,将数据分段存储,给每⼀段数据配⼀把锁,也就是所谓的锁分段技术。
传统方式 使用 HashTable 保证线程问题,是采用 synchronized 锁将整个 HashTable 中的数组锁住,
在多个线程中只允许一个线程访问 Put 或者 Get,效率非常低,但是能够保证线 程安全问题。

多线程的情况下 JDK 官方推荐使用 ConcurrentHashMap
ConcurrentHashMap 1.7 采用分段锁设计 底层实现原理:数组+Segments 分段锁 +HashEntry 链表实现 大致原理就是将一个大的 HashMap 分成 n 多个不同的小的 HashTable 不同的 key 计算 index 如果没有发生冲突 则存放到不同的小的 HashTable 中 , 从而可以实现多线程,同时做 put 操作,但是如果多个线程同时 put 操作 key 发 生了 index 冲突落到同一个小的 HashTable 中还是会发生竞争锁。
ConcurrentHashMap 1.7 采用 Lock 锁+CAS 乐观锁+UNSAFE 类 里面有实现 类 似于 synchronized 锁的升级过程。
ConcurrentHashMap 1.8 版本 put 操作 取消 segment 分段设计 直接使用 Node 数组来保存数据
index 没有发生冲突使用 cas 锁 index 如果发生冲突则 使用 synchronized在ConcurrentHashMap没有出现以前,jdk使用hashtable来实现线程安全,但是hashtable是将整个hash表锁住,所以效率很低下。

ConcurrentHashMap将数据分别放到多个Segment中,默认16个,每一个Segment中又包含了多个HashEntry列表数组,对于一个key,需要经过三次hash操作,才能最终定位这个元素的位置,这三次hash分别为:对于一个key,先进行一次hash操作,得到hash值h1,也即h1 = hash1(key);将得到的h1的高几位进行第二次hash,得到hash值h2,也即h2 = hash2(h1高几位),通过h2能够确定该元素的放在哪个Segment;将得到的h1进行第三次hash,得到hash值h3,也即h3 = hash3(h1),通过h3能够确定该元素放置在哪个HashEntry。每一个Segment都拥有一个锁,当进行写操作时,只需要锁定一个Segment,而其它Segment中的数据是可以访问的。

这样设计的好处是,使得锁的粒度相比Segment来说更小了,发生hash冲突 和 加锁的频率也降低了,在并发场景下的操作性能也提高了。而且,当数据量比较大的时候,查询性能也得到了很大的提升。
1.传统方式 使用 HashTable 保证线程问题,是采用 synchronized 锁将整个HashTable 中的数组锁住,在多个线程中只允许一个线程访问 Put 或者 Get,效率非常低,但是能够保证线程安全问题。
2.多线程的情况下 JDK 官方推荐使用 ConcurrentHashMap
ConcurrentHashMap 1.7 采用分段锁设计 底层实现原理:数组+Segments 分段锁+HashEntry 链表实现大致原理就是将一个大的 HashMap 分成 n 多个不同的小的 HashTable不同的 key 计算 index 如果没有发生冲突 则存放到不同的小的 HashTable 中 ,从而可以实现多线程,同时做 put 操作,但是如果多个线程同时 put 操作 key 发生了 index 冲突落到同一个小的 HashTable 中还是会发生竞争锁。
3.ConcurrentHashMap 1.7 采用 Lock 锁+CAS 乐观锁+UNSAFE 类 里面有实现 类似于 synchronized锁的升级过程。
4.ConcurrentHashMap 1.8 版本 put 操作 取消 segment 分段设计 直接使用 Node数组来保存数据
index 没有发生冲突使用 cas 锁 index 如果发生冲突则 使用 synchronized

JDK 1.7给Segment添加ReentrantLock锁来实现线程安全(分段锁)
JDK 1.8通过CAS或者synchronized来实现线程安全 (通过对指定的 Node 节点加锁)
JDK1.8 中,ConcurrentHashMap 锁的粒度是数组中的某一个节点,而在JDK1.7,锁定的是 Segment,锁的范围要更大
ConcurrentHashMap在JDK1.7中使用的是数组加链表结构,其中数组分为两大类,大数组是Segment,小数组是HashEntry。而加锁是通过Segment添加ReentrantLock重入锁来保证线程安全的。
ConcurrentHashMap在JDK1.8中使用的是数组加链表加红黑树的方式来实现。它是通过CAS或者synchronized来实现线程安全。并且也缩小了锁的粒度。查询性能也到了进一步的提升。

ConcurrentHashMap,在 JDK 1.7 中采用 分段锁的方式;
JDK 1.8 中直接采用了CAS(无锁算法)+ synchronized。
Java 7为实现并形访问,引⼊了Segment这⼀结构,实现了分段锁,理论上最⼤并发度与Segment个数相等。
Java 8为进⼀步提⾼并发性,摒弃了分段锁的⽅案,⽽是直接使⽤⼀个⼤的数组。同时为了提⾼哈希碰撞下的寻址性能,Java 8在链表⻓度超过⼀定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红⿊树(寻址时间复杂度为O(long(N) ))。其数据结构如下图所示

去 除 Segment + HashEntry + Unsafe的实现,
改 为 Synchronized + CAS + Node + Unsafe
其 实 Node 和 HashEntry 的内容⼀样,但是HashEntry是⼀个内部类。⽤ Synchronized + CAS 代 替 Segment ,这样锁的粒度更⼩了,并且不是每次都要加锁了,CAS尝试失败了在加锁。put( )⽅法中 初始化数组⼤⼩时,1.8不⽤加锁,因为⽤了个 sizeCtl变量,将这个变量置为-1,就表明table正在初始化。

JDK1.7:使用了分段锁机制实现 ConcurrentHashMap,ConcurrentHashMap 在对象中保存了一个 Segment 数组,即将整个 Hash 表划分为多个分段;而每个 Segment 元素,即每个分段则类似于一个 Hashtable;这样,在执行 put 操作时首先根据 hash 算法定位到元素属于哪个 Segment,然后对该Segment 加锁即可。因此ConcurrentHashMap 在多线程并发编程中可是实现多线程put 操作,不过其最大并发度受 Segment 的个数限制。
JDK1.8: 底层采用数组+链表+红黑树的方式实现,而加锁则采用 CAS 和 synchronized实现
ConcurrentHashMap 类(是 Java并发包 java.util.concurrent 中提供的一个线程安全且高效的 HashMap 实现)。HashTable 是使用 synchronize 关键字加锁的原理(就是对对象加锁);
ConcurrentHashMap,在 JDK 1.7 中采用 分段锁的方式;JDK 1.8 中直接采用了CAS(无锁算法)+ synchronized。

ConcurrentHashMap 的整体架构 ConcurrentHashMap 的基本功能 ConcurrentHashMap 在性能方面的优化
ConcurrentHashMap 的整体架构 这个是 ConcurrentHashMap 在 JDK1.8 中的存储结构,它是由数组、单向链表、红黑树组成。当我们初始化一个 ConcurrentHashMap 实例时,默认会初始化一个长度为 16的数组。由于ConcurrentHashMap 它的核心仍然是 hash 表,所以必然会存在hash 冲突问题。ConcurrentHashMap 采用链式寻址法来解决 hash 冲突。当 hash 冲 突 比 较 多 的 时 候 , 会 造 成 链 表 长 度 较 长 , 这 种 情 况 会 使 得ConcurrentHashMap 中数据元素的查询复杂度变成 O(n)。因此在 JDK1.8 中,引入了红黑树的机制。
当数组长度大于 64 并且链表长度大于等于 8 的时候,单项链表就会转换为红黑树。另外,随着 ConcurrentHashMap 的动态扩容,一旦链表长度小于 8,红黑树会退化成单向链表。

ConcurrentHashMap 本质上是一个 HashMap,因此功能和 HashMap 一样,但是 ConcurrentHashMap 在 HashMap 的基础上,提供了并发安全的实现。并发安全的主要实现是通过对指定的 Node 节点加锁,来保证数据更新的安全性

ConcurrentHashMap 在性能方面做的优化
如果在并发性能和数据安全性之间做好平衡,在很多地方都有类似的设计,比如cpu 的三级缓存、mysql 的 buffer_pool、Synchronized 的锁升级等等。
ConcurrentHashMap 也做了类似的优化,主要体现在以下几个方面:
在 JDK1.8 中,ConcurrentHashMap 锁的粒度是数组中的某一个节点,而在JDK1.7,锁定的是 Segment,锁的范围要更大,因此性能上会更低。引入红黑树,降低了数据查询的时间复杂度,红黑树的时间复杂度是O(logn)。当数组长度不够时,ConcurrentHashMap 需要对数组进行扩容,在扩容的实现上,ConcurrentHashMap 引入了多线程并发扩容的机制,简单来说就是多个线程对原始数组进行分片后,每个线程负责一个分片的数据迁移,从而提升了扩容过程中数据迁移的效率。ConcurrentHashMap 中有一个 size()方法来获取总的元素个数,而在多线程并发场景中,在保证原子性的前提下来实现元素个数的累加,性能是非常低的。
ConcurrentHashMap 在这个方面的优化主要体现在两个点:
当线程竞争不激烈时,直接采用 CAS 来实现元素个数的原子递增。如果线程竞争激烈,使用一个数组来维护元素个数,如果要增加总的元素个数,则直接从数组中随机选择一个,再通过 CAS 实现原子递增。它的核心思想是引入了数组来实现对并发更新的负载。比如锁粒度控制、分段锁的设计等,它们都可以应用在实际业务场景中。
Currenthashmap 的扩容机制
JDK1.7:
先对数组的长度增加一倍,然后遍历原来的旧的 table 数组,把每一个数组元素也就是 Node 链表迁移到新的数组里面,最后迁移完毕之后,把新数组的引用直接替换旧的。
JDK1.8:
扩容时候会判断这个值,如果超过阈值就要扩容,首先根据运算得到需要遍历的次数i,然后利用 tabAt 方法获得 i 位置的元素 f,初始化一个 forwardNode 实例 fwd,如果 f == null,则在 table 中的 i 位置放入 fwd,否则采用头插法的方式把当前旧table 数组的指定任务范围的数据给迁移到新的数组中,然后 给旧 table 原位置赋值fwd。直到遍历过所有的节点以后就完成了复制工作,把 table 指向 nextTable,并更新 sizeCtl 为新数组大小的 0.75 倍 ,扩容完成。在此期间如果其他线程的有读写操作都会判断 head 节点是否为 forwardNode 节点,如果是就帮助扩容。

Java 中 ConcurrentHashMap 的并发度是什么

ConcurrentHashMap 把实际 map 划分成若干部分来实现它的可扩展性和线程安全。这种划分是使用并发度获得的,它是 ConcurrentHashMap 类构造函数的一个可选参数,默认值为 16,这样在多线程情况下就能避免争用。在 JDK8 后,它摒弃了 Segment(锁段)的概念,而是启用了一种全新的方式
实现,利用 CAS 算法。同时加入了更多的辅助变量来提高并发度,具体内容还是查看源码吧。

ConcurrentHashMap 的并发度就是 segment 的大小,默认为 16,这意味着最多同时可以有16 条线程操作 ConcurrentHashMap,这也是 ConcurrentHashMap 对 Hashtable 的最大优势

ConcurrentHashMap 的 Key 和 Value 都不能为 null,而 HashMap 却可以,你知道这么设计的原因是什么吗?

ConcurrentMaps(ConcurrentHashMaps,ConcurrentSkipListMaps)不允许使用null的主要原因是,无法容纳在非并行映射中几乎无法容忍的歧义。最主要的是,如果map.get(key)return null,则无法检测到该键是否显式映射到null该键。在非并行映射中,您可以通过进行检查 map.contains(key),但在并行映射中,两次调用之间的映射可能已更改。
hashtable也是线程安全的,所以也是key和value也是不可以null的 treeMap 线程不安全,但是因为需要排序,进行key的compareTo方法,所以key是不能null中,value是可以的

因为给ConcurrentHashMap中插入 null (空)值会存在歧义。我们可以假设
ConcurrentHashMap允许插入 null(空) 值,那么,我们取值的时候会出现两种结果:
1、值没有在集合中,所以返回的结果就是 null (空);
2值就是 null(空),所以返回的结果就是它原本的 null(空) 值。

这就产生了歧义问题。
那HashMap允许插入 null(空) 值,难道它就不担心出现歧义吗?这是因为HashMap的设计是给
单线程使用的,所以如果取到 null(空) 值,我们可以通过HashMap的 containsKey(key)方 法来区分
这个 null(空) 值到底是插入值是 null(空),还是本就没有才返回的 null(空) 值。
而 ConcurrentHashMap 就不一样了,因为 ConcurrentHashMap 是在多线程场景下使用的,它的
情况更加复杂。举个例子,现在有线程T1调用了 ConcurrentHashMap 的 containsKey(key) 方法,我们期望返回的结果是false,也就是说,T1并没有往ConcurrentHashMap中 put null(空)值。
但是,恰恰出了个意外,在线程T1还没有得到返回结果之前,线程T2又调用了ConcurrentHashMap 的 put() 方法,插入了一个Key,并且存入的Value是 null(空) 值。那么,线程T1 最终得到的返回结果就变成 true 了。显然,这个结果和我们之前期望的 false 完全不一致。
也就是说,在多线程的复杂情况下,我们多线程的复杂情况下,到底是插入的 null(空) 值,还是
本就没有才返回的 null(空) 值。也就是说,产生的歧义不能被 证 伪,以上信件的主要意思是,Doug Lea 认为这样设计最主要的原因是:不容忍在并发场景下出现歧义

参考:
原文链接:https://blog.csdn.net/qq_42068856/article/details/126091526
https://blog.csdn.net/qq_18300037/article/details/123795776

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

思静语

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值