深入理解HashMap+ConcurrentHashMap的扩容策略

匿名 (未验证) 提交于 2019-12-02 20:41:15

前言

理解HashMap和ConcurrentHashMap的重点在于:

(1)理解HashMap的数据结构的设计和实现思路

(2)在(1)的基础上,理解ConcurrentHashMap的并发安全的设计和实现思路

前面的文章已经介绍过Map结构的底层实现,这里我们重点放在其扩容方法,
这里分别对JDK7和JDK8版本的HashMap+ConcurrentHashMap来分析:

JDK7的HashMap扩容



这个版本的HashMap数据结构还是数组+链表的方式,扩容方法如下:

Java代码
  1. null;



上面的这段代码不并不难理解,对于扩容操作,底层实现都需要新生成一个数组,然后拷贝旧数组里面的每一个Node链表到新数组里面,这个方法在单线程下执行是没有任何问题的,但是在多线程下面却有很大问题,主要的问题在于基于头插法的数据迁移,会有几率造成链表倒置,从而引发链表闭链,导致程序死循环,并吃满CPU。据说已经有人给原来的SUN公司提过bug,但sun公司认为,这是开发者使用不当造成的,因为这个类本就不是线程安全的,你还偏在多线程下使用,这下好了吧,出了问题这能怪我咯?仔细想想,还有点道理。


JDK7的ConcurrentHashMap扩容

HashMap是线程不安全的,我们来看下线程安全的ConcurrentHashMap,在JDK7的时候,这种安全策略采用的是分段锁的机制,ConcurrentHashMap维护了一个Segment数组,Segment这个类继承了重入锁ReentrantLock,并且该类里面维护了一个

Java代码


注意这里面的代码,外部已经加锁,所以这里面是安全的,我们看下具体的实现方式:先对数组的长度增加一倍,然后遍历原来的旧的table数组,把每一个数组元素也就是Node链表迁移到新的数组里面,最后迁移完毕之后,把新数组的引用直接替换旧的。此外这里这有一个小的细节优化,在迁移链表时用了两个for循环,第一个for的目的是为了,判断是否有迁移位置一样的元素并且位置还是相邻,根据HashMap的设计策略,首先table的大小必须是2的n次方,我们知道扩容后的每个链表的元素的位置,要么不变,要么是原table索引位置+原table的容量大小,举个例子假如现在有三个元素(3,5,7)要放入map里面,table的的容量是2,简单的假设元素位置=元素的值 % 2,得到如下结构:

Java代码
  1. [0]=
  2. [1]=3->5->



现在将table的大小扩容成4,分布如下:

Java代码
  1. [0]=
  2. [1]=5->
  3. [2]=
  4. [3]=


因为扩容必须是2的n次方,所以HashMap在put和get元素的时候直接取key的hashCode然后经过再次均衡后直接采用&位运算就能达到取模效果,这个不再细说,上面这个例子的目的是为了说明扩容后的数据分布策略,要么保留在原位置,要么会被均衡在旧的table位置,这里是1加上旧的table容量这是是2,所以是3。基于这个特点,第一个for循环,作的优化如下,假设我们现在用0表示原位置,1表示迁移到index+oldCap的位置,来代表元素:

Java代码
  1. [0]=
  2. [1]=0->1->1->0->0->0->


第一个for循环的会记录lastRun,比如要迁移[1]的数据,经过这个循环之后,lastRun的位置会记录第三个0的位置,因为后面的数据都是0,代表他们要迁移到新的数组中同一个位置中,所以就可以把这个中间节点,直接插入到新的数组位置而后面附带的一串元素其实都不需要动。

接着第二个循环里面在此从第一个0的位置开始遍历到lastRun也就是第三个元素的位置就可以了,只循环处理前面的数据即可,这个循环里面根据位置0和1做不同的链表追加,后面的数据已经被优化的迁移走了,但最坏情况下可能后面一个也没优化,比如下面的结构:

Java代码
  1. [0]=
  2. [1]=1->1->0->0->0->0->1->



这种情况,第一个for循环没多大作用,需要通过第二个for循环从头开始遍历到尾部,按0和1分发迁移,这里面使用的是还是头插法的方式迁移,新迁移的数据是追加在链表的头部,但这里是线程安全的所以不会出现循环链表,导致死循环问题。迁移完成之后直接将最新的元素加入,最后将新的table替换旧的table即可。


JDK8的HashMap扩容


在JDK8里面,HashMap的底层数据结构已经变为数组+链表+红黑树的结构了,因为在hash冲突严重的情况下,链表的查询效率是O(n),所以JDK8做了优化对于单个链表的个数大于8的链表,会直接转为红黑树结构算是以空间换时间,这样以来查询的效率就变为O(logN),图示如下:



我们看下其扩容代码:

Java代码
  1. @SuppressWarnings({"rawtypes",


在JDK8中,单纯的HashMap数据结构增加了红黑树是一个大的优化,此外根据上面的迁移扩容策略,我们发现JDK8里面HashMap没有采用头插法转移链表数据,而是保留了元素的顺序位置,新的代码里面采用:

Java代码


把要迁移的元素分类之后,最后在分别放到新数组对应的位置上:

Java代码


JDK7里面是先判断table的存储元素的数量是否超过当前的threshold=table.length*loadFactor(默认0.75),如果超过就先扩容,在JDK8里面是先插入数据,插入之后在判断下一次++size的大小是否会超过当前的阈值,如果超过就扩容。



JDK8的ConcurrentHashMap扩容



在JDK8中彻底抛弃了JDK7的分段锁的机制,新的版本主要使用了Unsafe类的CAS自旋赋值+synchronized同步+LockSupport阻塞等手段实现的高效并发,代码可读性稍差。

ConcurrentHashMap的JDK8与JDK7版本的并发实现相比,最大的区别在于JDK8的锁粒度更细,理想情况下talbe数组元素的大小就是其支持并发的最大个数,在JDK7里面最大并发个数就是Segment的个数,默认值是16,可以通过构造函数改变一经创建不可更改,这个值就是并发的粒度,每一个segment下面管理一个table数组,加锁的时候其实锁住的是整个segment,这样设计的好处在于数组的扩容是不会影响其他的segment的,简化了并发设计,不足之处在于并发的粒度稍粗,所以在JDK8里面,去掉了分段锁,将锁的级别控制在了更细粒度的table元素级别,也就是说只需要锁住这个链表的head节点,并不会影响其他的table元素的读写,好处在于并发的粒度更细,影响更小,从而并发效率更好,但不足之处在于并发扩容的时候,由于操作的table都是同一个,不像JDK7中分段控制,所以这里需要等扩容完之后,所有的读写操作才能进行,所以扩容的效率就成为了整个并发的一个瓶颈点,好在Doug lea大神对扩容做了优化,本来在一个线程扩容的时候,如果影响了其他线程的数据,那么其他的线程的读写操作都应该阻塞,但Doug lea说你们闲着也是闲着,不如来一起参与扩容任务,这样人多力量大,办完事你们该干啥干啥,别浪费时间,于是在JDK8的源码里面就引入了一个ForwardingNode类,在一个线程发起扩容的时候,就会改变sizeCtl这个值,其含义如下:

Java代码
  1. -
  2. 2、如果table初始化完成,表示table的容量,默认是table大小的


扩容时候会判断这个值,如果超过阈值就要扩容,首先根据运算得到需要遍历的次数i,然后利用tabAt方法获得i位置的元素f,初始化一个forwardNode实例fwd,如果f == null,则在table中的i位置放入fwd,否则采用头插法的方式把当前旧table数组的指定任务范围的数据给迁移到新的数组中,然后
给旧table原位置赋值fwd。直到遍历过所有的节点以后就完成了复制工作,把table指向nextTable,并更新sizeCtl为新数组大小的0.75倍 ,扩容完成。在此期间如果其他线程的有读写操作都会判断head节点是否为forwardNode节点,如果是就帮助扩容。

扩容源码如下:

Java代码
  1. @SuppressWarnings(



在扩容时读写操作如何进行



(1)对于get读操作,如果当前节点有数据,还没迁移完成,此时不影响读,能够正常进行。

如果当前链表已经迁移完成,那么头节点会被设置成fwd节点,此时get线程会帮助扩容。


(2)对于put/remove写操作,如果当前链表已经迁移完成,那么头节点会被设置成fwd节点,此时写线程会帮助扩容,如果扩容没有完成,当前链表的头节点会被锁住,所以写线程会被阻塞,直到扩容完成。


对于size和迭代器是弱一致性



volatile修饰的数组引用是强可见的,但是其元素却不一定,所以,这导致size的根据sumCount的方法并不准确。

同理Iteritor的迭代器也一样,并不能准确反映最新的实际情况

总结


本文主要了介绍了HashMap+ConcurrentHashMap的扩容策略,扩容的原理是新生成大于原来1倍大小的数组,然后拷贝旧数组数据到新的数组里面,在多线程情况下,这里面如果注意线程安全问题,在解决安全问题的同时,我们也要关注其效率,这才是并发容器类的最出色的地方。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!