美文网首页源码分析java并发编程Java整理
深入分析ConcurrentHashMap1.8的扩容实现

深入分析ConcurrentHashMap1.8的扩容实现

作者: 美团Java | 来源:发表于2017-03-09 17:00 被阅读11438次

简书 占小狼 转载请注明原创出处,谢谢!

此谓知本,此谓知之至也 《礼记·大学》


1、深入浅出ConcurrentHashMap(1.8)
2、谈谈ConcurrentHashMap1.7和1.8的不同实现
3、ConcurrentHashMap的红黑树实现分析

ConcurrentHashMap相关的文章写了不少,有个遗留问题一直没有分析,也被好多人请教过,被搁置在一旁,即如何在并发的情况下实现数组的扩容。

什么情况会触发扩容

当往hashMap中成功插入一个key/value节点时,有可能触发扩容动作:
1、如果新增节点之后,所在链表的元素个数达到了阈值 8,则会调用treeifyBin方法把链表转换成红黑树,不过在结构转换之前,会对数组长度进行判断,实现如下:

如果数组长度n小于阈值MIN_TREEIFY_CAPACITY,默认是64,则会调用tryPresize方法把数组长度扩大到原来的两倍,并触发transfer方法,重新调整节点的位置。

2、新增节点之后,会调用addCount方法记录元素个数,并检查是否需要进行扩容,当数组元素个数达到阈值时,会触发transfer方法,重新调整节点的位置。

transfer实现

transfer方法实现了在并发的情况下,高效的从原始组数往新数组中移动元素,假设扩容之前节点的分布如下,这里区分蓝色节点和红色节点,是为了后续更好的分析:

在上图中,第14个槽位插入新节点之后,链表元素个数已经达到了8,且数组长度为16,优先通过扩容来缓解链表过长的问题,实现如下:
1、根据当前数组长度n,新建一个两倍长度的数组nextTable

2、初始化ForwardingNode节点,其中保存了新数组nextTable的引用,在处理完每个槽位的节点之后当做占位节点,表示该槽位已经处理过了;

3、通过for自循环处理每个槽位中的链表元素,默认advace为真,通过CAS设置transferIndex属性值,并初始化ibound值,i指当前处理的槽位序号,bound指需要处理的槽位边界,先处理槽位15的节点;

4、在当前假设条件下,槽位15中没有节点,则通过CAS插入在第二步中初始化的ForwardingNode节点,用于告诉其它线程该槽位已经处理过了;

5、如果槽位15已经被线程A处理了,那么线程B处理到这个节点时,取到该节点的hash值应该为MOVED,值为-1,则直接跳过,继续处理下一个槽位14的节点;

6、处理槽位14的节点,是一个链表结构,先定义两个变量节点lnhn,按我的理解应该是lowNodehighNode,分别保存hash值的第X位为0和1的节点,具体实现如下:

使用fn&n可以快速把链表中的元素区分成两类,A类是hash值的第X位为0,B类是hash值的第X位为1,并通过lastRun记录最后需要处理的节点,A类和B类节点可以分散到新数组的槽位14和30中,在原数组的槽位14中,蓝色节点第X为0,红色节点第X为1,把链表拉平显示如下:

1、通过遍历链表,记录runBitlastRun,分别为1和节点6,所以设置hn为节点6,ln为null;
2、重新遍历链表,以lastRun节点为终止条件,根据第X位的值分别构造ln链表和hn链表:

ln链:和原来链表相比,顺序已经不一样了


hn链:


通过CAS把ln链表设置到新数组的i位置,hn链表设置到i+n的位置;

7、如果该槽位是红黑树结构,则构造树节点lohi,遍历红黑树中的节点,同样根据hash&n算法,把节点分为两类,分别插入到lohi为头的链表中,根据lohi链表中的元素个数分别生成lnhn节点,其中ln节点的生成逻辑如下:
(1)如果lo链表的元素个数小于等于UNTREEIFY_THRESHOLD,默认为6,则通过untreeify方法把树节点链表转化成普通节点链表;
(2)否则判断hi链表中的元素个数是否等于0:如果等于0,表示lo链表中包含了所有原始节点,则设置原始红黑树给ln,否则根据lo链表重新构造红黑树。

最后,同样的通过CAS把ln设置到新数组的i位置,hn设置到i+n位置。

相关文章

网友评论

  • 3a1271aceb39:看懂了 虽然有一些地方还不够详细
  • 2c1f2b8e36af:里面有错
    "通过CAS把ln链表设置到新数组的i位置,hn链表设置到i+n的位置;"
    这里并没有使用CAS,源码是使用了putObjectVolatile,意思是直接把数据写回主内存
  • 10e69d8fb9c9:5、如果槽位15已经被线程A处理了,那么线程B处理到这个节点时,取到该节点的hash值应该为MOVED,值为-1,则直接跳过,继续处理下一个槽位14的节点;
    我想问一下,线程B在分配迁移槽的时候并不会分配之前分配过的啊,通过了transferindex, 那么这边理论上不会出现某个线程检测到自己要处理的范围有已经被其他线程处理过的啊?不知道你知道这个条件在什么时候有用?
    e380688e348b:你好,我一开始也有跟你一样的疑问,确实扩容时每个线程处理的范围不会重叠,其实这个MOVED标志是给在扩容的同时准备put的其他线程用的。put的时候如果遇到MOVED,说明正在扩容并且当前的ENTRY已经被处理过了,所以put线程加入一起扩容。如果没有遇到MOVED,说明当前ENTRY没有被处理过,即使正在扩容也不管,直接锁住当前ENTRY,先put完再判断当前是否在扩容,是的话put线程也会加入一起扩容。所以1.8中扩容和PUT操作是可以并发执行的,Doug Lea大神真是把CAS玩的出神入化。
  • 6fe71063a4a0:哇 写的太好了!! 完美解决了我的疑惑。但是区分红蓝节点那一块,table数组长度=16时,应该去判断hash值的第五位吧?16-1=15,二进制1111,定位到扩容数组里时应该判断第四位的左边一位也就是第五位是0还是1。
  • brycegao:setTabAt(nextTab, i, ln); //低位
    setTabAt(nextTab, i + n, hn); //高位
    还有一点很重要:扩展时每个线程只能修改i或i+n桶位置, 保证了线程互斥,即每个桶位置只能有1个线程修改。 例如现在有16个桶, 需要transfer到32个桶。 每个线程只能修改第0/16, 1/16+1, 2/16+2,.... 或15/16+15位置的桶。
  • brycegao:ConcurrentHash扩容跟HashMap类似, 因为高位bit值0/1的概率是50%, 所以链表/红黑树均匀散列到高位或低位。
    ConcurrentHashMap通过CAS比较桶节点实现线程互斥, 在扩容时可以多线程并发; 但HashMap resize时只能单线程
  • 3c05e2bc096a:狼哥,你还是没有解释为什么构造一个反序列表放在i+n上啊
    83b3ccec058e::disappointed_relieved: i+n的 ho链表是正序的, 但是 ln的链表反序了, 可以看看Node构造函数
  • cd13856c86e2:感谢,结合源码在过两遍
  • 何甜甜在吗:hello,有一处我不是很明白,就是它通过cas获得tab[i]上的元素。因为table为volatile的,当对数组中的元素进行了修改,需要立即写回工作内存,其他线程工作内存中存储该地址的table数组将无效,那当其他线程需要获得table[i]时就必须往住内存中读取了。为什么还是使用cas,感觉volatile能够保证获得最新值啊
    12f1ea5a45f5:@我是皮皮甜大王 不知这样说能否解答您的疑问:volatile可以保证可见性,有序性,但是不能保证绝对的有序性,比如i++这个过程是 用javap看是有三步。而cas就是一种无锁的同步保证,性能比锁好
  • 5fa1243ea3a3:请问resizeStamp这个方法返回的指是什么,返回值在addCount中的那段比较是什么意思。
  • a3b65c108416:你好,请教一个问题:
    文中提到达到数组阈值时,会进行扩容。

    代码中通过s>sizeCTL,判断是否进行扩容。

    我测试发现,put了两个值(两个值的hash相等),此时数组里只有一个桶有值,并且是个链表。但即使两个值都在一个hash桶里,s却add了两次,变成了2。所以这个阈值到底是数组的阈值,还是node总个数的阈值呢?
    a3b65c108416:@大火燎原 我觉得应该是map里的node总个数>数组*0.75,会执行扩容,而不是数组里不为null的桶的个数>数组*0.75
  • 右丶羽:哈哈,好难懂呀,勉强看懂1.7,1.8不同实现那一篇
  • 936a6ae986e6:狼哥,hn链顺序没变是不是碰巧,假如现在2是红色的话,那最后hn链是不是就是4->2->6->7->8 了
  • 1ab793c447f6:也就是说table最大能达到64?
    美团Java:@邱勇Aaron whywhy23
    码农皮邱:小狼可以加个QQ或者微信好友吗?多与你交流一下。
    可以留一个联系方式吗?
    美团Java:@张宏伟_6661 不是,table可以一直扩容
  • liboren:还是没太明白为什么ln链:和原来链表相比,顺序已经不一样了
    极简架构:setTabAt(nextTab, i, ln);
    setTabAt(nextTab, i + n, hn); 请教下为什么 这个要将 ln 与后值为0 放入14 ,与后值为1 放入 30这两个位置,这个是后续要做什么处理? 这块没太get到求教
    极简架构:runBit和lastRun 这块 0 ,1 这块没太懂,他们作用是什么? 0,1是指 hash & n 与的结果只有 0,1是吧
    美团Java:@liboren 多看几次
  • 91c2c44739c2:我想请问一下啊,ForwardingNode节点保存nextTable的引用,这个的作用是什么?
    美团Java:@Bun_ 用于并发移动时,其它线程可以知道这个节点正在被移动,或已经被移动
  • d8ced77a0050:3.5年工作经验,都这么厉害了,是在惭愧啊
  • Sundial丶Dreams:总结一下就是,去掉segment锁改为cas保证写,多于8采用红黑树优化查询。
    美团Java:@Sundial丶Dreams 多谢肯定,3.5年
    Sundial丶Dreams:@占小狼 所以我说的是总结一下,我是从你的Java面试那篇文章过来的,写的很棒!方便透露下您工作了多少年吗?
    美团Java:@Sundial丶Dreams 这么说也没毛病,但是区别的有点简单
  • 慧明小和尚下山去化斋:狼哥,ConcurrentHashMap使用的tabAt看的不是太懂,很奇怪它的偏移量为什么那样算,您有研究过吗
    美团Java:@意大利炮的二营长 哈哈,这个和jvm中的内存有关,数组的偏移量就是这样实现的
  • 36028fe7cc9f:大神,这些代码怎么看啊,好难看懂。再问下,平时如何调试多线程代码呢?
    美团Java:@普罗米修斯是谁 就是正常的debug
    36028fe7cc9f: @占小狼 平时调试多线程有啥技巧没
    美团Java:@普罗米修斯是谁 耐心看:smile:
  • wcyong:不错
    美团Java:@wcyong 多谢肯定
  • 开发者头条_程序员必装的App:感谢分享!已推荐到《开发者头条》:https://toutiao.io/posts/e53a9y 欢迎点赞支持!
    欢迎订阅《java进阶之路》https://toutiao.io/subjects/85249
  • 风中的小狮子:和1.7不一样了
    美团Java:恩,1.7和1.8是实现完全不一样了

本文标题:深入分析ConcurrentHashMap1.8的扩容实现

本文链接:https://www.haomeiwen.com/subject/sqemgttx.html