针对扩容和put的原理,这个问题,我在学习的时候,一直有一个疑问点:假如当前A线程已经对15这个位置扩完容了,那如果这时候B线程又对15这个位置插入了元素,要怎么处理?
通过对代码的仔细查看学习,找到了这个问题的答案

在扩容的时候,假如线程A对i这个位置扩容完成了,那线程A会把原数组中i位置,放入一个ForwardingNode对象,放入的这个对象很有讲究,也起到了很大的作用,这个和put时的一些逻辑处理有关系

扩容的思想

先说扩容的思想
在concurrentHashMap中,如果当前线程A触发了扩容的机制,线程A会开始进行扩容,在扩容的时候,涉及到一个步伐的概念,简单来说,就是加入了辅助扩容的机制,所以,此时一个线程会负责一部分数据的扩容,举个简单例子来说
1.假如当前数组中有32个位置,那线程A负责31到16这区间的元素的扩容
2.如果此时线程B来put元素,发现要put到30这个位置,发现正在被扩容,那此时就会去辅助扩容,所谓的辅助扩容,就是线程B负责15到0这个区间的扩容

在源码中,会计算一个线程所负责扩容的大小,是根据CPU核数来计算的,最小是16个,也就是说,一个线程最少负责16个位置元素的扩容

java.util.concurrent.ConcurrentHashMap#transfer/**
* 1.这里的stride是步长,这个步长最小值是16
*/
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)stride = MIN_TRANSFER_STRIDE; // subdivide range

这里是计算步长的公式,NCPU是当前CPU的核数,n是当前老数组的长度,会根据这个公式去计算
1.如果cpu核数大于1,那就(n >>> 3) / NCPU 以这个值作为步长
2.如果CPU核数小于等于1,那就以数组长度作为步长
3.如果这样计算得到的步长依旧小于16,那就把16作为步长

也就是说,如果CPU核数只有1个,并且当前数组长度大于16,那就以数组长度作为步长;如果CPU核数有多个,就会根据一个公式,计算一个阈值,如果这个值小于16,就以16作为步长


/*** 2.对新的table进行初始化和赋值* 默认扩容之后的table长度是原table的一倍* nextTab就是扩容之后的数组*/
if (nextTab == null) {            // initiatingtry {@SuppressWarnings("unchecked")Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];nextTab = nt;} catch (Throwable ex) {      // try to cope with OOMEsizeCtl = Integer.MAX_VALUE;return;}nextTable = nextTab;/*** 这里的n是扩容之前,数组的长度*/transferIndex = n;
}

如果线程A来扩容,会计算出来当前线程A所要负责扩容的区间范围

问题

我一直疑惑的一个问题是:
1.假如线程A正在对31到16位置的数组进行扩容,此时已经把31、30位置完成了扩容,正在扩容29这个位置
2.此时线程B来put数据,正好计算得到要插入到31这个位置,并且31这个位置在扩容之前就是空,那此时怎么处理?

我一直迷惑的地方在这里,在没有搞懂源码之前,我一直认为,这种场景有问题,但是实际看了代码之后,解答了我的疑惑

下面这个是put的源码

/*** 这里的:*  i:表示根据key的hash值计算得到的需要存放到哪个元素下*  f:表示i下标中对应的元素结点,要么是树的根节点,要么是链表的头结点,要么就只有这一个元素*  fh:表示f结点的hash值* @param key* @param value* @param onlyIfAbsent* @return*/
final V putVal(K key, V value, boolean onlyIfAbsent) {if (key == null || value == null) throw new NullPointerException();int hash = spread(key.hashCode());int binCount = 0;//1.在put元素的时候,首先判断当前tab是否为空,为空,就初始化for (Node<K,V>[] tab = table;;) {Node<K,V> f; int n, i, fh;if (tab == null || (n = tab.length) == 0)tab = initTable();//2.如果tab不为空,就根据key计算出一个下标值,判断数组中这个位置是否为null,为null,就new一个新的node节点,通过cas设置到该数组对应的元素中else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))break;                   // no lock when adding to empty bin}/*** 3.这里的f就是根据put的key计算得到的元素下标位置,如果hash值为-1,表示当前有其他线程正在进行扩容* 如果有其他线程在扩容,那helpTransfer的意思是帮助另外一个线程去扩容** 这里要帮助扩容是这样的:*  如果A线程正在对数组进行扩容,会把A线程自己正在扩容的tab[8]位置的元素的hash设置为moved*  1、如果B线程在put元素的时候,正好是要放到tab[8]这个位置的,那此时就没办法插入了,因为A线程正在迁移这个位置的元素,所以:*  干脆B线程就帮助A线程一起去迁移整个数组*      等数组迁移完成了,B线程就会再来一遍循环,此时获取到的table,就是新的table,就可以进行加锁、put数据等*  2、如果B线程在put元素的时候,是要放到tab[7]位置,那此时是不受影响的(我这里只是举例子,只说思想),就可以继续对tab[7]加锁,然后进行put*  此时就是A线程一遍迁移tab[8]位置的元素,B线程一遍向tab[7]位置插入元素*/else if ((fh = f.hash) == MOVED)tab = helpTransfer(tab, f);else {//4.进入到这里,表示是正常的插入V oldVal = null;synchronized (f) {/*** 4.1 先通过synchronized加锁* 4.2 然后再判断当前i这个位置是否是f节点,因为有可能当前线程在执行的时候,其他线程修改了数组中i位置对应的结点信息* 4.3 如果hashCode大于等于0,表示这是一个链表*   4.3.1 从头结点开始循环,如果找到key相同的node结点,就把值放到oldVal,然后value覆盖原来的value*   4.3.2 如果到最后一个结点,还没有找到key相等的,就插入到队尾* 4.4 如果当前node结点是TreeBin类型的,那就是树** 4.5 如果bigCount大于0,且大于链表转红黑树的阈值,就进行树的转换* 如果oldValue不为null,表示是值覆盖,就返回oldValue*/if (tabAt(tab, i) == f) {if (fh >= 0) {binCount = 1;for (Node<K,V> e = f;; ++binCount) {K ek;//这里的if判断,是进行覆盖操作if (e.hash == hash &&((ek = e.key) == key ||(ek != null && key.equals(ek)))) {oldVal = e.val;if (!onlyIfAbsent)e.val = value;break;}Node<K,V> pred = e;//如果遍历到尾结点还是没有查到相同的key,就插入到尾结点的后面if ((e = e.next) == null) {pred.next = new Node<K,V>(hash, key,value, null);break;}}}// 如果当前元素存储的是红黑树else if (f instanceof TreeBin) {Node<K,V> p;binCount = 2;if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,value)) != null) {oldVal = p.val;if (!onlyIfAbsent)p.val = value;}}}}if (binCount != 0) {/*** 判断是否需要进行树化* 如果oldVal不为null,表示是进行了覆盖写入,直接return 即可*/if (binCount >= TREEIFY_THRESHOLD)treeifyBin(tab, i);if (oldVal != null)return oldVal;break;}}}/*** 将concurrentHashMap维护的元素个数 + 1,在这个方法中,可能会触发扩容的逻辑*/addCount(1L, binCount);return null;
}

在注释中第2点,i = (n - 1) & hash假如这个计算得到的i是31,那如果31已经扩容完成了,那old数组中31这个位置不是null,而是有一个ForwardingNode对象,所以,对于上面说的这个场景,在第二点注释的这里,if判断条件一定不会成功,所以,此时线程B会进入到下一个else判断,也就是

 else if ((fh = f.hash) == MOVED)tab = helpTransfer(tab, f);

会进入到这里,去进行辅助扩容

为什么?

为什么线程B在往31位置插入元素的时候,会进入到辅助扩容这里?因为在扩容的时候,有一个细节点:
线程A在对i这个位置进行扩容,和hashmap中的扩容的思想基本上 是一样,会将该位置分为高低两个链表去存入到新的数组中,但是在将高低链表插入到新的数组中之后,还会额外做一个操作,在old数组中的,将i这个位置设置一个变量

ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);

上面截图中的代码,是在扩容做的操作,其中最后一步,是将fwd通过cas设置到old数组中i位置,所以说:
1、线程A在对i位置扩容完成之后,i位置就存储了fwd元素
2、在put的时候,会对i位置进行判断,源码中是判断是否为null,如果i位置已经扩容完成了,在i位置插入的时候,就会判断到i位置不为null
3、就会接着判断i位置元素的hash值是否为moved,如果是,就开始辅助扩容的逻辑

所以,结论就有了,如果i这个位置已经完成了扩容,在old数组中,该位置并不是null,而是存储了一个ForwardingNode对象,这个对象的hash值是moved(-1);所以当有线程往该位置插入元素的时候,是会进入到辅助扩容的逻辑中去处理

concurrentHashMap扩容细节相关推荐

  1. 【面试篇】ConcurrentHashMap1.8 扩容细节

    ConcurrentHashMap1.8 扩容细节 [面试篇]数据结构-哈希表 [面试篇]HashMap常见面试题目 [面试篇]HashMap1.7和HashMap1.8的详细区别对比 [面试篇]Co ...

  2. redis的hash怎么实现以及 rehash过程是怎样的?和JavaHashMap的rehash有什么区别,与ConcurrentHashMap扩容的策略比较?

    文章为自己不懂的时候,搜集网络相关内容的综合理解,非原创.有些内容稍有改动. =========================================================== 2 ...

  3. ConcurrentHashMap扩容原理

    前言 ConcurrentHashMap从名称是可以看出,它是一个HashMap而且是线程安全的.在多线程编程中使用非常广泛.ConcurrentHashMap的实现方式,在jdk6,7,8中都不一样 ...

  4. ConcurrentHashMap 实现细节(转)

    ConcurrentHashMap是Java 5中支持高并发.高吞吐量的线程安全HashMap实现.在这之前我对ConcurrentHashMap只有一些肤浅的理解,仅知道它采用了多个锁,大概也足够了 ...

  5. 大厂之路一由浅入深、并行基础、源码分析一 “J.U.C”之collections框架:ConcurrentHashMap扩容迁移等方法的源码分析

    参考文献: 本篇文章主要来源于这篇博客,本人对ConcurrentHashMap的源码之前没有学,虽然看了书但是不涉及这些,所以大部分都来源于观看大神博客的感悟,如果大家需要,可以看一看,也可以点击跳 ...

  6. jdk 1.8 concurrenthashmap扩容原理

    https://www.cnblogs.com/yangchunchun/p/7279881.html

  7. 理解Java7和8里面HashMap+ConcurrentHashMap的扩容策略

    ### 前言 理解HashMap和ConcurrentHashMap的重点在于: (1)理解HashMap的数据结构的设计和实现思路 (2)在(1)的基础上,理解ConcurrentHashMap的并 ...

  8. 并发编程——ConcurrentHashMap#transfer() 扩容逐行分析

    并发编程--ConcurrentHashMap#transfer() 扩容逐行分析 </h1><div class="clear"></div> ...

  9. ConcurrentHashMap底层详解(图解扩容)(JDK1.8)

    数据结构 使用数组+链表+红黑树来实现,利用 CAS + synchronized 来保证并发更新的安全 源码分析 put方法 public V put(K key, V value) {return ...

  10. ConcurrentHashMap核心原理,这次彻底给整明白了

    ConcurrentHashMap,它在技术面试中出现的频率相当之高,所以我们必须对它深入理解和掌握. 谈到 ConcurrentHashMap,就一定会想到 HashMap.HashMap 在我们的 ...

最新文章

  1. shell win10 改成cmd_win10远程ipconfigs闪退win+r解决查看地址
  2. 一个简单的PHP模板引擎
  3. Chrome浏览器扩展开发系列之五:Page Action类型的Chrome浏览器扩展
  4. 8.4 matlab用户界面设计工具
  5. 一篇文章带你搞定Python返回函数
  6. python编码效率高吗_【原创】杠精的日常-讨论python快排的效率
  7. win10 php7+apache2.4的配置以及遇到的问题及解决
  8. Semantic Role Labeling (SRL)
  9. js判断ie 火狐 还是chrome浏览器
  10. Android so 文件全部报错:Duplicate resources
  11. c语言方程没有解,【C语言】一元二次方程的解
  12. 《沉思录》读书精摘——对伦理学的古典思考
  13. Gossip算法详解
  14. 计算机硬件介绍之CPU与多线程
  15. 7.Android常用第三方支付
  16. linux命令查询端口号,linux查询端口号(linux查看端口的命令)
  17. 数据结构:利用栈实现数制转换
  18. Jacoco-报告改造实践
  19. 集成电机驱动方案(STM32+DRV8841)
  20. MongoDB 存放图片

热门文章

  1. mac sublime text 3 列操作,替换相同内容, 用动态输入的方式
  2. 算法:求数的幂次方powx-n
  3. 图像分类数据集(Fashion-MNIST)
  4. 首届电子商务AI算法大赛 Organized by automlai
  5. List之LinkedList与ArrayList区别
  6. matplot画图控制marker点的个数_专刊主编述评 中药质量标志物(Qmarker):提高中药质量标准及质量控制理论和促进中药产业科学发展...
  7. 凸优化第二章凸集 2.4广义不等式
  8. 【从线性回归到BP神经网络】第三部分:Logistic回归
  9. POJ1321-Chess Problem(dfs基础题)
  10. 多面集的表示定理的必要性的证明