当数组中存放的节点个数超过了阈值就可以任务当前的哈希冲突可能性较大,HashMap就会对当前数组进行扩容操作。在普通的HashMap中就会直接声明一个两倍大小的新数组,然后从旧数组中从左到右进行转化,但是在多线程下肯定会存在线程安全的问题。那么ConcurrentHashMap是如何来保证多线程下的扩容安全的呢?

transfer是进行扩容操作的方法,接收两个参数tab(原容器数组对象),nextTab(新容器数组对象,一般长度为tab的两倍)。

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {int n = tab.length, stride;// stride : 单个线程需要处理的下标数// 当有多个CPU参与扩容时,每个CPU处理的数量就是(n / 8 / NPU)// 如果单个CPU处理的数量小于MIN_TRANSFER_STRIDE(16),就取最小值MIN_TRANSFER_STRIDEif ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)stride = MIN_TRANSFER_STRIDE; // subdivide range// 当扩容刚开始时nextTab为null,进行初始化操作if (nextTab == null) {            // initiating// 创建一个新的2倍长度的心数组try {@SuppressWarnings("unchecked")Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];nextTab = nt;} catch (Throwable ex) {      // try to cope with OOME//   如果扩容所需空间过大导致异常,就将阈值设置为最大值,停止扩容sizeCtl = Integer.MAX_VALUE;return;}nextTable = nextTab;transferIndex = n;}// 扩容后数组长度int nextn = nextTab.length;// 定义ForwardingNode类并设置状态为MOVED,该类指向新的扩容数组ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);// 标识是否需要boolean advance = true;// 标志扩容是否结束boolean finishing = false; // to ensure sweep before committing nextTabfor (int i = 0, bound = 0;;) {Node<K,V> f; int fh;// 循环查找需要进行处理的下标while (advance) {int nextIndex, nextBound;// 当前所分配的数组区段还未完成或者扩容已完成,停止推进if (--i >= bound || finishing)advance = false;// 没有需要分配的内容else if ((nextIndex = transferIndex) <= 0) {i = -1;advance = false;}// 尝试获取某个区间段扩容任务else if (U.compareAndSwapInt(this, TRANSFERINDEX, nextIndex,nextBound = (nextIndex > stride ?nextIndex - stride : 0))) {// 代表任务的结束下标bound = nextBound;// 任务的起始下标(可以理解为数组的从右往左遍历)i = nextIndex - 1;// 已找到要处理的下标,停止推进advance = false;}}// 表示当前所需处理的数组下标不合法,可能已经完成了扩容操作if (i < 0 || i >= n || i + n >= nextn) {int sc;// 已完成扩容,将用户访问的数组指向新数组,并重新设置阈值if (finishing) {nextTable = null;table = nextTab;sizeCtl = (n << 1) - (n >>> 1);return;}// 在完成后将sizeCtl - 1if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {// (sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT 代表其他扩容执行的线程都已经执行完成,// 符合就将扩容状态标志置为trueif ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)return;  finishing = advance = true;// 重置i等于原数组长度    重新检查所有的内容i = n; // recheck before commit}}// 如果i的位置为null,直接将null放入fwd中else if ((f = tabAt(tab, i)) == null)advance = casTabAt(tab, i, null, fwd);// 如果MOVED说明已经处理过了else if ((fh = f.hash) == MOVED)advance = true; // already processedelse {synchronized (f) {// 其他线程没有修改执行下标元素if (tabAt(tab, i) == f) {Node<K,V> ln, hn;if (fh >= 0) {int runBit = fh & n;Node<K,V> lastRun = f;// 先将最后一部分进行划分for (Node<K,V> p = f.next; p != null; p = p.next) {int b = p.hash & n;if (b != runBit) {runBit = b;lastRun = p;}}if (runBit == 0) {ln = lastRun;hn = null;}else {hn = lastRun;ln = null;}for (Node<K,V> p = f; p != lastRun; p = p.next) {int ph = p.hash; K pk = p.key; V pv = p.val;//   利用hash值通过头插法生成左右两个链表if ((ph & n) == 0)ln = new Node<K,V>(ph, pk, pv, ln);elsehn = new Node<K,V>(ph, pk, pv, hn);}setTabAt(nextTab, i, ln);setTabAt(nextTab, i + n, hn);setTabAt(tab, i, fwd);advance = true;}else if (f instanceof TreeBin) {TreeBin<K,V> t = (TreeBin<K,V>)f;TreeNode<K,V> lo = null, loTail = null;TreeNode<K,V> hi = null, hiTail = null;int lc = 0, hc = 0;for (Node<K,V> e = t.first; e != null; e = e.next) {int h = e.hash;TreeNode<K,V> p = new TreeNode<K,V>(h, e.key, e.val, null, null);if ((h & n) == 0) {if ((p.prev = loTail) == null)lo = p;elseloTail.next = p;loTail = p;++lc;}else {if ((p.prev = hiTail) == null)hi = p;elsehiTail.next = p;hiTail = p;++hc;}}ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :(hc != 0) ? new TreeBin<K,V>(lo) : t;hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :(lc != 0) ? new TreeBin<K,V>(hi) : t;setTabAt(nextTab, i, ln);setTabAt(nextTab, i + n, hn);setTabAt(tab, i, fwd);advance = true;}}}}}
}

扩容流程

首次开启扩容任务的线程:

  1. 调用transfer方法,其中nextTab参数为null;

  2. 根据CPU数及当前线程数,计算出每个线程所要负责的数组区间长度值stride(最小值为MIN_TRANSFER_STRIDE,16);

  3. 创建一个数组长度为原来两倍的新数组,将新数组放入nextTable变量中,并设置transferIndex为原数组长度值(转换由原数组从右往左执行);

    3.1 由于新数组长度可能过大导致出现OOM异常,说明已达到最大长度,直接将阈值设置为int的最大值,停止后续的扩容操作;

  4. 根据当前未转化区间的最大值以及stride,计算出转化的起始下标值和终止下标值;

  5. 按照起始下标值进行循环遍历转换;

  6. 创建ForwardingNode对象fwd,参数为新数组,同时节点的hash值为MOVED

  7. 当前下标值非法;

    6.1 如果finishing为true,代表整个扩容操作已完成,将table指向新数组,nextTab设置为null,阈值sizeCtl为原数组长度的1.75倍;

    6.2 如果finishing为false,代表当前线程的扩容任务已完成,并且已无可分配的区间,将sizeCtl进行减1;

    ​ 6.2.2 当(sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT 为true,代表还有其他线程正在进行扩容操作,直接结束当前方法;

    ​ 6.2.3 反之为false,说明当前是完成扩容任务的最后一个线程,将finishing置为true,并完成后续的检查以及数组替换任务;

  8. 当前下标节点对象为null,通过cas操作将fwd对象放入该下标处;

  9. 当前下标节点的hash为MOVED,代表当前位置已完成转换,进行下一轮循环;

  10. 对当前下标节点对象加锁;

    10.1 如果对应节点对象为改变,按照HashMap的扩容思想(根据最高位hash值,分为左右两部分)进行扩容。

其他线程对扩容时的操作:

putVal

  1. 获取到对应的下标节点对象;

  2. 如果节点对象为null,直接进行将新的节点放入;

  3. 如果节点的hash值为MOVED代表该下标节点已完成扩容转化任务,帮助其他线程完成剩余扩容任务;

    3.1 获取到节点中存放的新数组对象,将sizeCtl的值+1,然后按照区间分配的原则进行扩容任务;

  4. 如果不满足上面两种情况,按照正常的插入操作进行。

get

  1. 返回null

1. ConcurrentHashMap如何保证扩容安全?

  • cas操作保证只有一个线程进行新数组的初始化工作;

  • 将整个数组分为若干个区间,每个区间都只对应单独的线程;

  • 转化前在当前下标节点上加synchronized锁;

  • 插入操作检测到对应的节点处于MOVED状态,先完成扩容在进行插入。

2. 如何判断扩容任务完成?

扩容任务刚开始时设置sizeCtl为 (rs << RESIZE_STAMP_SHIFT) + 2),后续有新的线程加入参与扩容任务就将sizeCtl + 1,线程当前分配的区间已转化完成且没有可以分配的区间,此时将sizeCtl - 1。如果最后sizeCtl 等于 (rs << RESIZE_STAMP_SHIFT) + 2) 说明扩容完成。

细说ConcurrentHashMap扩容规则相关推荐

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

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

  2. ConcurrentHashMap扩容原理

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

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

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

  4. jdk 1.8 concurrenthashmap扩容原理

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

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

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

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

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

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

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

  8. 【java】ConcurrentHashMap遍历 --- 弱一致性的迭代器(Iterator)实现原理

    1.概述 转载:ConcurrentHashMap遍历 - 弱一致性的迭代器(Iterator)实现原理 在 Java并发编程实战(进阶篇) 中分析了 Vector 在迭代过程中对容器进行修改会抛出 ...

  9. ConcurrentHashMap面试灵魂拷问,你能扛多久

    点击关注公众号,实用技术文章及时了解 前言 本文从 ConcurrentHashMap 常见的面试问题引入话题,并逐步揭开其设计原理,相信读完本文,对面试中的相关问题会有很大的帮助. HashMap ...

  10. 深入理解HashMap+ConcurrrentHashMap扩容的原理

    目录 1.JDK1.7版本的HashMap扩容机制(重要) 2.JDK1.8版本的HashMap扩容机制(重要) 2.JDK1.7版本的ConcurrentHashMap扩容机制 4.JDK1.8版本 ...

最新文章

  1. React Native 项目简单整理-组件优化
  2. 妇女在IT安全工作人员当中的比例只有10%
  3. Spark SQL入门示例
  4. 安装hadoop-2.3.0-cdh5.1.2全过程
  5. 基于VMware Workstation创建虚拟机,以Ubuntu16.04为例
  6. Hibernate 主清单文件配制说明
  7. np python_python小白之np功能快速查
  8. linux - tar压缩解压缩使用,快速记忆方法
  9. Centos 7忘记密码,如何重置
  10. C语言程序的开发过程
  11. 修改Dreamweaver(DW) cs6代码背景为黑色
  12. SQL Server数据库第二课:创建数据库表、完善数据库表的设计、建立数据库表之间的关系
  13. 时钟软件哪个好?9款苹果电脑时钟提醒软件推荐
  14. 无线网络安全与解决方案(Wireless Security)
  15. Excel多个sheet导出pdf
  16. Excel 文件的扩展名 .xls 与 .xlsx 的区别
  17. java中args是什么意思?
  18. c语言程序延时10s,单片机C语言程序设计:10s 的秒表
  19. 新零售模式你知道多少?教你沉浸式体验
  20. 数字图像处理:实验三 图像增强

热门文章

  1. ftp服务器复制文件命令,FTP服务器的Copy命令的使用
  2. MFC基于对话框程序启动时隐藏窗口的实现
  3. 新一配:iTunes的历史版本【转载】
  4. Android随笔之——PackageManager详解
  5. FatFs- 通用FAT文件系统模块
  6. python爬虫做毕业论文_基于Python的网络爬虫(智联招聘)开发与实现毕业论文+作品源码+演示视频...
  7. 【金融人士工具大全】整理不易,且珍惜
  8. 手工编译Flex SDK 多国语言包
  9. DIY WINDOWS XP
  10. [ZT]Grub4dos for WinPE 启动菜单