做者:炸鸡可乐
原文出处:www.pzblog.cnhtml
在以前的集合文章中,咱们了解到 HashMap 在多线程环境下操做可能会致使程序死循环的线上故障!java
既然在多线程环境下不能使用 HashMap,那若是咱们想在多线程环境下操做 map,该怎么操做呢?node
想必阅读太小编以前写的《HashMap 在多线程环境下操做可能会致使程序死循环》一文的朋友们必定知道,其中有一个解决办法就是使用 java 并发包下的 ConcurrentHashMap 类!git
今天呢,咱们就一块儿来聊聊 ConcurrentHashMap 这个类!数组
众所周知,在 Java 中,HashMap 是非线程安全的,若是想在多线程下安全的操做 map,主要有如下解决方法:安全
Hashtable
线程安全类;Collections.synchronizedMap
方法,对方法进行加同步锁;ConcurrentHashMap
类;在以前的文章中,关于 Hashtable 类,咱们也有所介绍,Hashtable 是一个线程安全的类,Hashtable 几乎全部的添加、删除、查询方法都加了synchronized
同步锁!数据结构
至关于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操做该对象,那其余线程只能阻塞等待须要的锁被释放,在竞争激烈的多线程场景中性能就会很是差,因此 Hashtable 不推荐使用!多线程
再来看看第二种方法,使用Collections.synchronizedMap
方法,咱们打开 JDK 源码,部份内容以下:并发
能够很清晰的看到,若是传入的是 HashMap 对象,其实也是对 HashMap 作的方法作了一层包装,里面使用对象锁来保证多线程场景下,操做安全,本质也是对 HashMap 进行全表锁!ide
使用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 的源码实现。
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(初始化容量)为1六、loadFactor(负载因子)为0.7五、concurrentLevel(并发等级)为16,若是不指定则会使用默认值。
其中,值得注意的是 concurrentLevel 这个参数,虽然 Segment 数组大小 ssize 是由 concurrentLevel 来决定的,可是却不必定等于 concurrentLevel,ssize 经过位移动运算,必定是大于或者等于 concurrentLevel 的最小的 2 的次幂!
经过计算能够看出,按默认的 initialCapacity 初始容量为16,concurrentLevel 并发等级为16,理论上就容许 16 个线程并发执行,而且每个线程独占一把锁访问 Segment,不影响其它的 Segment 操做!
从以前的文章中,咱们了解到 HashMap 在多线程环境下操做可能会致使程序死循环,仔细想一想你会发现,形成这个问题无非是 put 和扩容阶段发生的!
那么这样咱们就能够从 put 方法下手了,来看看 ConcurrentHashMap 是怎么操做的?
ConcurrentHashMap 的 put 方法,源码以下:
从源码能够看出,这部分的 put 操做主要分两步:
真正插入元素的 put 方法,源码以下:
从源码能够看出,真正的 put 操做主要分如下几步:
scanAndLockForPut
方法,这个方法也是尝试获取对象锁;咱们再来看看,上面提到的scanAndLockForPut
这个方法,源码以下:
scanAndLockForPut
这个方法,操做也是分如下几步:
lock()
方法获取对象锁,若是依然没有获取到,当前线程就阻塞,直到获取以后退出循环;经过scanAndLockForPut()
方法,当前线程就能够在即便获取不到segment
锁的状况下,完成须要添加节点的实例化工做,当获取锁后,就能够直接将该节点插入链表便可。
这个方法还实现了相似于自旋锁的功能,循环式的判断对象锁是否可以被成功获取,直到获取到锁才会退出循环,防止执行 put 操做的线程频繁阻塞,这些优化都提高了 put 操做的性能。
get 方法就比较简单了,由于不涉及增、删、改操做,因此不存在并发故障问题,源码以下:
因为 HashEntry 涉及到的共享变量都使用 volatile 修饰,volatile 能够保证内存可见性,因此不会读取到过时数据。
remove 操做和 put 方法差很少,都须要获取对象锁才能操做,经过 key 找到元素所在的 Segment 对象而后移除,源码以下:
与 get 方法相似,先获取 Segment 数组所在的 Segment 对象,而后经过 Segment 对象去移除元素,源码以下:
先获取对象锁,若是获取到以后执行移除操做,以后的操做相似 hashMap 的移除方法,步骤以下:
虽然 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 方法!
打开 JDK1.8 中的 ConcurrentHashMap 中的 put 方法,源码以下:
当进行 put 操做时,流程大概能够分以下几个步骤:
f
,在当前数组下标是否第一次插入,若是是就经过 CAS 方式插入;f.hash == -1
是否成立,若是成立,说明当前f
是ForwardingNode
节点,表示有其它线程正在扩容,则一块儿进行扩容操做;Node
节点按链表或红黑树的方式插入到合适的位置;8
,若是超过8
个,就将链表转化为红黑树结构;put 操做大体的流程,就是这样的,能够看的出,复杂程度比 JDK1.7 上了一个台阶。
咱们再来看看源码中的第3步 initTable()
方法,若是数组为空就初始化数组,源码以下:
sizeCtl 是一个对象属性,使用了volatile关键字修饰保证并发的可见性,默认为 0,当第一次执行 put 操做时,经过Unsafe.compareAndSwapInt()
方法,俗称CAS
,将 sizeCtl
修改成 -1
,有且只有一个线程可以修改为功,接着执行 table 初始化任务。
若是别的线程发现sizeCtl<0
,意味着有另外的线程执行CAS操做成功,当前线程经过执行Thread.yield()
让出 CPU 时间片等待 table 初始化完成。
咱们继续来看看 put 方法中第5步helpTransfer()
方法,若是f.hash == -1
成立,说明当前f
是ForwardingNode
节点,意味有其它线程正在扩容,则一块儿进行扩容操做,源码以下:
这个过程,操做步骤以下:
sizeCtl < 0
,说明还在扩容;sizeCtl + 1
, 增长了一个线程帮助其扩容;咱们再来看看源码中的第9步 addCount()
方法,插入完成以后,扩容判断,源码以下:
这个过程,操做步骤以下:
put 的流程基本分析完了,能够从中发现,里面大量的使用了CAS
方法,CAS 表示比较与替换,里面有3个参数,分别是目标内存地址、旧值、新值,每次判断的时候,会将旧值与目标内存地址中的值进行比较,若是相等,就将新值更新到内存地址里,若是不相等,就继续循环,直到操做成功为止!
虽然使用的了CAS
这种乐观锁方法,可是里面的细节设计的很复杂,阅读比较费神,有兴趣的朋友们能够本身研究一下。
get 方法操做就比较简单了,由于不涉及并发操做,直接查询就能够了,源码以下:
从源码中能够看出,步骤以下:
reomve 方法操做和 put 相似,只是方向是反的,源码以下:
replaceNode 方法,源码以下:
从源码中能够看出,步骤以下:
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 要复杂的多。
本文由于是断断续续写出来,若是有理解不对的地方,欢迎各位网友指出!
一、JDK1.7&JDK1.8 源码
二、JavaGuide - 容器 - ConcurrentHashMap