CHM的使用算法
ConcurrentHashMap是J.U.C包里面提供的一个线程安全而且高效的HashMap,因此ConcurrentHashMap在并发编程的场景中使用的频率比较高,那么这一节课咱们就从ConcurrentHashMap的使用上以及源码层面来分析ConcurrentHashMap究竟是如何实现安全性的编程
api使用api
ConcurrentHashMap是Map的派生类,因此api基本和Hashmap是相似,主要就是put、get这些方法,接下来基于ConcurrentHashMap的put和get这两个方法做为切入点来分析ConcurrentHashMap的源码实现数组
先要作一个说明,这节课分析的ConcurrentHashMap是基于Jdk1.8的版本。安全
JDK1.7和Jdk1.8版本的变化数据结构
ConcurrentHashMap和HashMap的实现原理是差很少的,可是由于ConcurrentHashMap须要支持并发操做,因此在实现上要比hashmap稍微复杂一些。多线程
在JDK1.7的实现上,ConrruentHashMap由一个个Segment组成,简单来讲,ConcurrentHashMap是一个Segment数组,它经过继承ReentrantLock来进行加锁,经过每次锁住一个segment来保证每一个segment内的操做的线程安全性从而实现全局线程安全。整个结构图以下架构
当每一个操做分布在不一样的segment上的时候,默认状况下,理论上能够同时支持16个线程的并发写入。并发
相比于1.7版本,它作了两个改进app
这个结构和JDK1.8版本中的Hashmap的实现结构基本一致,可是为了保证线程安全性,ConcurrentHashMap的实现会稍微复杂一下。接下来咱们从源码层面来了解一下它的原理.
咱们基于put和get方法来分析它的实现便可
假如在上面这段代码中存在两个线程,在不加锁的状况下:线程A成功执行casTabAt操做后,随后的线程B能够经过tabAt方法马上看到table[i]的改变。缘由以下:线程A的casTabAt操做,具备volatile读写相同的内存语义,根据volatile的happens-before规则:线程A的casTabAt操做,必定对线程B的tabAt操做可见
initTable
数组初始化方法,这个方法比较简单,就是初始化一个合适大小的数组
sizeCtl这个要单独说一下,若是没搞懂这个属性的意义,可能会被搞晕
这个标志是在Node数组初始化或者扩容的时候的一个控制位标识,负数表明正在进行初始化或者扩容操做
-1 表明正在初始化
-N 表明有N-1有二个线程正在进行扩容操做,这里不是简单的理解成n个线程,sizeCtl就是-N,这块后续在讲扩容的时候会说明
0标识Node数组尚未被初始化,正数表明初始化或者下一次扩容的大小
tabAt
该方法获取对象中offset偏移地址对应的对象field的值。实际上这段代码的含义等价于tab[i], 可是为何不直接使用tab[i]来计算呢?
getObjectVolatile,一旦看到volatile关键字,就表示可见性。由于对volatile写操做happen-before于volatile读操做,所以其余线程对table的修改均对get读取可见;
虽然table数组自己是增长了volatile属性,可是“volatile的数组只针对数组的引用具备volatile的语义,而不是它的元素”。 因此若是有其余线程对这个数组的元素进行写操做,那么当前线程来读的时候不必定能读到最新的值。
出于性能考虑,Doug Lea直接经过Unsafe类来对table进行操做。
图解分析
在putVal方法执行完成之后,会经过addCount来增长ConcurrentHashMap中的元素个数,而且还会可能触发扩容操做。这里会有两个很是经典的设计
addCount
在putVal最后调用addCount的时候,传递了两个参数,分别是1和binCount(链表长度),看看addCount方法里面作了什么操做
x表示此次须要在表中增长的元素个数,check参数表示是否须要进行扩容检查,大于等于0都须要进行检查
CounterCells解释
ConcurrentHashMap是采用CounterCell数组来记录元素个数的,像通常的集合记录集合大小,直接定义一个size的成员变量便可,当出现改变的时候只要更新这个变量就行。为何ConcurrentHashMap要用这种形式来处理呢?
问题仍是处在并发上,ConcurrentHashMap是并发集合,若是用一个成员变量来统计元素个数的话,为了保证并发状况下共享变量的的难全兴,势必会须要经过加锁或者自旋来实现,若是竞争比较激烈的状况下,size的设置上会出现比较大的冲突反而影响了性能,因此在ConcurrentHashMap采用了分片的方法来记录大小,具体什么意思,咱们来分析下
fullAddCount源码分析
fullAddCount主要是用来初始化CounterCell,来记录元素个数,里面包含扩容,初始化等操做
初始化CounterCells数组
CounterCells初始化图解
初始化长度为2的数组,而后随机获得指定的一个数组下标,将须要新增的值加入到对应下标位置处
判断是否须要扩容,也就是当更新后的键值对总数baseCount >= 阈值sizeCtl时,进行rehash,这里面会有两个逻辑。
resizeStamp
这块逻辑要理解起来,也有一点复杂。
resizeStamp用来生成一个和扩容有关的扩容戳,具体有什么做用呢?咱们基于它的实现来作一个分析
transfer
扩容是ConcurrentHashMap的精华之一,扩容操做的核心在于数据的转移,在单线程环境下数据的转移很简单,无非就是把旧数组中的数据迁移到新的数组。可是这在多线程环境下,在扩容的时候其余线程也可能正在添加元素,这时又触发了扩容怎么办?可能你们想到的第一个解决方案是加互斥锁,把转移过程锁住,虽然是可行的解决方案,可是会带来较大的性能开销。由于互斥锁会致使全部访问临界区的线程陷入到阻塞状态,持有锁的线程耗时越长,其余竞争线程就会一直被阻塞,致使吞吐量较低。并且还可能致使死锁。
而ConcurrentHashMap并无直接加锁,而是采用CAS实现无锁的并发同步策略,最精华的部分是它能够利用多线程来进行协同扩容
简单来讲,它把Node数组看成多个线程之间共享的任务队列,而后经过维护一个指针来划分每一个线程锁负责的区间,每一个线程经过区间逆向遍从来实现扩容,一个已经迁移完的bucket会被替换为一个ForwardingNode节点,标记当前bucket已经被其余线程迁移完了。接下来分析一下它的源码实现
一、fwd:这个类是个标识类,用于指向新表用的,其余线程遇到这个类会主动跳过这个类,由于这个类要么就是扩容迁移正在进行,要么就是已经完成扩容迁移,也就是这个类要保证线程安全,再进行操做。
二、advance:这个变量是用于提示代码是否进行推动处理,也就是当前桶处理完,处理下一个桶的标识
三、finishing:这个变量用于提示扩容是否结束用的
扩容过程图解
ConcurrentHashMap支持并发扩容,实现方式是,把Node数组进行拆分,让每一个线程处理本身的区域,假设table数组总长度是64,默认状况下,那么每一个线程能够分到16个bucket。
而后每一个线程处理的范围,按照倒序来作迁移
经过for自循环处理每一个槽位中的链表元素,默认advace为真,经过CAS设置transferIndex属性值,并初始化i和bound值,i指当前处理的槽位序号,bound指须要处理的槽位边界,先处理槽位31的节点; (bound,i) =(16,31) 从31的位置往前推进。
假设这个时候ThreadA在进行transfer,那么逻辑图表示以下
在当前假设条件下,槽位15中没有节点,则经过CAS插入在第二步中初始化的ForwardingNode节点,用于告诉其它线程该槽位已经处理过了;
sizeCtl扩容退出机制
在扩容操做transfer的第2414行,代码以下
高低位原理分析
ConcurrentHashMap在作链表迁移时,会用高低位来实现,这里有两个问题要分析一下
如何实现高低位链表的区分 假如咱们有这样一个队列
第14个槽位插入新节点以后,链表元素个数已经达到了8,且数组长度为16,优先经过扩容来缓解链表过长的问题,扩容这块的图解稍后再分析,先分析高低位扩容的原理
假如当前线程正在处理槽位为14的节点,它是一个链表结构,在代码中,首先定义两个变量节点ln和hn,实际就是lowNode和HighNode,分别保存hash值的第x位为0和不等于0的节点
经过fn&n能够把这个链表中的元素分为两类,A类是hash值的第X位为0,B类是hash值的第x位为不等于0(至于为何要这么区分,稍后分析),而且经过lastRun记录最后要处理的节点。最终要达到的目的是,A类的链表保持位置不动,B类的链表为14+16(扩容增长的长度)=30
咱们把14槽位的链表单独伶出来,咱们用蓝色表示 fn&n=0的节点,假如链表的分类是这样
接着,经过CAS操做,把hn链放在i+n也就是14+16的位置,ln链保持原来的位置不动。而且设置当前节点为fwd,表示已经被当前线程迁移完了
迁移完成之后的数据分布以下
为何要作高低位的划分
要想了解这么设计的目的,咱们须要从ConcurrentHashMap的根据下标获取对象的算法来看,在putVal方法中1018行
扩容结束之后的退出机制
若是线程扩容结束,那么须要退出,就会执行transfer方法的以下代码
若是对应的节点存在,判断这个节点的hash是否是等于MOVED(-1),说明当前节点是ForwardingNode节点,
意味着有其余线程正在进行扩容,那么当前如今直接帮助它进行扩容,所以调用helpTransfer方法
这个方法的主要做用是,若是被添加的节点的位置已经存在节点的时候,须要以链表的方式加入到节点中
若是当前节点已是一颗红黑树,那么就会按照红黑树的规则将当前节点加入到红黑树中
判断链表的长度是否已经达到临界值8. 若是达到了临界值,这个时候会根据当前数组的长度来决定是扩容仍是将链表转化为红黑树。也就是说若是当前数组的长度小于64,就会先扩容。不然,会把当前链表转化为红黑树
treeifyBin
在putVal的最后部分,有一个判断,若是链表长度大于8,那么就会触发扩容或者红黑树的转化操做。
tryPresize
tryPresize里面部分代码和addCount的部分代码相似,看起来会稍微简单一些
文章中涉及到的技术点我都分享在Java架构社区 142019080 里或者+V:JaneS0307,录制成视频供你们免费下载,但愿能够帮助在这个行 业发展的朋友和童鞋们,在论坛博客等地方少花些时间找资料,把有限的时间,真正花在学习上,因此我把这些资料, 分享出来。相信对于已经工做和遇到技术瓶颈或者写博客码友,在这份资料中必定都有你须要的内容。