理解ConcurrentHashMap

理解ConcurrentHashMap,第1张

理解ConcurrentHashMap

文章目录

前言一、ConcurrentHashMap引入原因

1.1线程不安全的HashMap1.2效率低下的HashTable容器 二、ConcurrentHashMap的实现

2.1减小锁粒度2.2JDK1.72.3JDK1.82.4ConcurrentHashMap1.7和1.8的区别 三、如何提高ConcurrentHashMap效率总结


前言

众所周知HashMap虽然效率高,但不是线程安全的,存在多线程并发下的安全隐患,而HashTable虽然是线程安全的,但锁的粒度太大(get/put使用synchronized锁住整个Hash表),效率非常低下(只能有一个线程 *** 作)。ConcurrentHashMap减小了锁的粒度,保证线程安全的前提下,效率也能得到保障。


一、ConcurrentHashMap引入原因 1.1线程不安全的HashMap

1.HashMap在jdk1.8之前,向链表添加元素时采用的是头插法,多线程 *** 作链表会发生环化,此时产生死循环。

尽管jdk1.8修复了该问题(采用尾插法),但jdk1.8也会出现死循环,只是原因不同:
(1)链表转换为树。
(2)对树进行 *** 作时。

2.HashMap是一个线程不安全的集合,如果在遍历的过程中同时对该集合进行修改 *** 作,例如put,add,remove等,可能会抛出java.util.ConcurrentModificationException(并发修改异常 )。


1.2效率低下的HashTable容器

HashTable是线程安全的。但是get/put所有相关 *** 作都是synchronized的,这相当于个整个Hash表加了一把大锁,锁的粒度特别的大,效率将大打折扣。

多线程访问时,只要以后一个线程访问或 *** 作该对象,那其他线程只能阻塞,相当于所有的 *** 作串行化,在竞争激烈的多线程场景下,性能就会表现的非常差。


二、ConcurrentHashMap的实现

Java 5.0 在 java.utilconcurrent 包中提供了多种并发容器类来改进同步容器的性能。

ConcurrentHashMap同步容器类是 Java 5 增加的一个线程安全的哈希表。对与多线程的 *** 作,介于 HashMap 与 Hashtable 之间。

2.1减小锁粒度

减小锁粒度是指通过缩小锁定对象的范围来减少锁冲突的可能性,最终提高系统的并发能力。

减小锁粒度是一种削弱多线程锁竞争的有效方法,ConcurrentHashMap并发下的安全机制就是基于该方法实现的。


2.2JDK1.7

jdk1.7中concurrentHashMap在内部采用分段锁机制,使用了多个SegMent,在 *** 作时会给Segment都加锁,这样通过减少锁粒度提高了并发度。

在 *** 作ConcurrentHashMap时,如果需要在其中添加一个新的数据,而不是对整个表都加锁,而是先根据HashCode查询该数据应该被存放在哪个段,然后对该段数据加锁完成put *** 作,这样在线程间就可以做到并行的线程安全。


2.3JDK1.8

jdk1.8 弃用了分段锁,使用 cas+synchronized,进而提高性能。

为什么要放弃分段锁?
1.加入多个分段锁浪费内存空间。
2.map 在放入时竞争同一个锁的概率非常小,分段锁反而会造成更新等 *** 作的长时间等待。

jdk1.8使用了 Node 锁,减低锁的粒度,并使用CAS *** 作来确保Node的一些 *** 作的原子性,取代了锁。

1.put 时首先通过hash找到对应链表过后,查看是否是第一个 Node,如果是,直接用 CAS原则插入,无需加锁。

2.如果不是链表第一个 Node,则直接用链表第一个 Node 加锁,这里加的锁是synchronized

为什么使用synchronized而不用ReentrantLock?

1.减少内存开销
如果使用ReentrantLock则需要节点继承AQS来获得同步支持,增加内存开销,而1.8中只有头节点需要进行同步。

2.内部优化
synchronized则是JVM直接支持的,JVM能够在运行时作出相应的优化措施:锁粗化、锁消除、锁自旋等等。


2.4ConcurrentHashMap1.7和1.8的区别

1.思路不同
jdk1.7使用了锁分段机制,jdk1.8放弃了分段锁使用 cas+synchronized。

2.整体结构不同
jdk1.7:Segment + HashEntry + Unsafe
jdk1.8: Synchronized + CAS + Node + Unsafe

3.put()方法不同
jdk1.7:先定位Segment,再定位桶,put全程加锁,没有获取锁的线程提前找桶的位置,并最多自旋64次获取锁,超过则挂起。
jdk1.8:由于移除了Segment,类似HashMap,可以直接定位到桶,拿到first节点后进行判断1、为空则CAS插入 2、为-1则说明在扩容,则跟着一起扩容 3、else则加锁put(类似1.7)

4、resize()方法不同
jdk1.7:跟HashMap步骤一样,只不过是搬到单线程中执行,避免了HashMap在1.7中扩容时死循环的问题,保证线程安全。
jdk1.8:支持并发扩容,HashMap扩容在1.8中由头插改为尾插,ConcurrentHashmap也是,迁移也是从尾部开始,扩容前在桶的头部放置一个hash值为-1的节点,这样别的线程访问时就能判断是否该桶已经被其他线程处理过了。

5、size()方法不同
jdk1.7:很经典的思路:计算两次,如果不变则返回计算结果,若不一致,则锁住所有的Segment求和。
jdk1.8:用baseCount来存储当前的节点个数,这就设计到baseCount并发环境下修改的问题。


三、如何提高ConcurrentHashMap效率

如何在很短的时间内将大量数据插入到ConcurrentHashMap,提高ConcurrentHashMap的插入效率,可以从以下几个方面解答。

1.尽量散列均匀,减少hash冲突。

2.主要还是要通过配置合理的容量大小和扩容因子,尽可能减少扩容事件的发生。

3.锁资源的争夺:在put方法中会使用synchonized对头节点进行加锁,而锁本身也是分等级的,因此主要思路就是尽可能的避免锁等级。可以将数据通过通过ConcurrentHashMap的spread方法进行预处理,这样可以将存在hash冲突的数据放在一个组里面,每个组都使用单线程进行put *** 作,这样的话可以保证锁仅停留在偏向锁这个级别,不会升级,从而提升效率。


总结

在学习ConcurrentHashMap之前建议读者先学习HashMap和HashTable的相关内容,为学习ConcurrentHashMap打下良好的基础。本文的重点是ConcurrentHashMap的实现,在jdk1.7和jdk1.8中两者的实现原理有很大的区别,一定要避免混淆。在以后还将介绍更多的同步容器类,这些同步类的学习建立在之前常用集合类的基础之上,可以很好地锻炼我们并发编程的思维。


欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5718772.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存