前言一、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中两者的实现原理有很大的区别,一定要避免混淆。在以后还将介绍更多的同步容器类,这些同步类的学习建立在之前常用集合类的基础之上,可以很好地锻炼我们并发编程的思维。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)