ConcurrentHashMap的transfer阅读

news2024/9/21 11:11:28

[TOC]

流程图

concurrentHashMap#transfer

ConcurrenthashMaptransfer 主要是用于扩容重组阶段,当内部数组的容量值超过阈值时,将触发扩容重组, transfer 是该过程的主要实现。

相关概念

  1. ConcurrentHashMap 中,使用一个字段复用了多种功能,如:阈值控制、内部 Node[] 数组状态控制、扩容线程控制 等,该字段就是 sizeCtl
/**
 * 默认为0,用来控制table的初始化和扩容操作
 * -1: 代表table正在初始化
 * -N: 取-N对应的二进制的低16位数值为 M=(sizeCtl&31),此时有M-1个线程进行扩容
 * 
 * 其余情况:
 * 1、如果table未初始化,表示table需要初始化的大小。
 * 2、如果table初始化完成,表示table的容量,默认是table大小的0.75倍
 */
 private transient volatile int sizeCtl;
  1. ConcurrentHashMap 在重组时,做法与 HashMap 类似,但是具体新的数组,则是使用了内部一个数组变量 nextTable 以保证并发控制。其他如:链表的重组、树结构的重组 流程均是大同小异。
  2. ConcurrentHashMap 的重组采用了跟分段表类似的思想,实际上是将数组划分为不同的分段区间,如果有线程进入,可获取该区间辅助转换。
  3. transferIndexConcurrentHashMap 的内部属性,主要是在重组阶段中使用,用来表示还未被转换的数组,区间为:table[0] ~ table[transferIndex-1]
  4. ConcurrentHashMap 并发转换的过程,借助了 信号量 的概念,只有获取到信号的线程,才能进入辅助转换,而 信号量 则存储在 sizeCtl,每当一个线程进入获取,则 sizeCtl + 1(首个线程开启转换则是 sizeCtl + 2)。主要注意的是,该信号量的初始值为 负数,加入线程将增大 sizeCtl,直到 sizeCtl 的增大达到 0 时,信号量将用完,默认的与 信号量 相加等于 0 的值是:65534,也就是说,最多允许 65534 条线程参与辅助转换(非固定,可调节)。所以可通过 rs + 1 ~ rs + 65534 的边界控制,来决定线程是否加入辅助转换。让 sizeCtl 成为负数变成信号量的主要代码是: resizeStamp(n) << RESIZE_STAMP_SHIFT
  5. ConcurrentHash 的转换过程中,用到的辅助属性有两个:nextTabletransferIndex,它们属于线程共享的,所以在对他们进行变更时,都是使用了 “自旋/死循环 + CAS” 的方式,实现线程并发安全。

解析

转换过程 transfer 的每个调用入口,实际上外部都有对 sizeCtl 进行 自旋 + CAS 的操作。也就是并发情况下,即使多条线程想要进行扩容,那也只有一条线程能够成功,另外的线程则进入辅助扩容的过程

addCount

扩容方法进入前的判断如下:

private final void addCount(long x, int check) {
    // 省略部分代码...
    // nt -> nextTable
    // n -> num,sc -> sizeCtl
    Node<K, V>[] tab, nt;
    int n, sc;
    // 当前存储大于 75%,且总大小小于最大容量,需要扩容
    while (s >= (long) (sc = sizeCtl) && (tab = table) != null && (n = tab.length) < MAXIMUM_CAPACITY) {
        // resizeStamp 纯粹只是移位来保证右 16 位为0,可用来控制作为线程最大数
        // 左 16 位实际并没有保留太多信息(因为明显:resizeStamp(4)、resizeStamp(5)、resizeStamp(6)、(7)
        // 是相同的结果
        int rs = resizeStamp(n) << RESIZE_STAMP_SHIFT;
        if (sc < 0) {
            // 限制线程的最大或最小,当达到最大 65534(默认) 或 1 条时,则直接跳出
            // rs + 1 --> 最少线程数(相当于不正确的情况了,或者是初始化,因为起始时最少是 rs + 2)
            // rs + MAX_RESIZERS --> 最多线程数
            // 或其他情况,则不再辅助转移,如:nextable 已为 null 或 transferIndex <= 0(说明已结束)
            // 前两个条件是限制线程数,后两个条件是扩容已经结束
            if (sc == rs + MAX_RESIZERS || sc == rs + 1 || (nt = nextTable) == null || transferIndex <= 0)
                break;
            if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1))
                transfer(tab, nt);
        }
        // 如果 sc >= 0,说明是刚开始,
        // 因为 sc < 0 时,低16位表示有多少条线程在进行转移:sizeCtl & 31 - 1
        // 所以这里要 rs + 2
        else if (U.compareAndSetInt(this, SIZECTL, sc, rs + 2))
            transfer(tab, null);
        s = sumCount();
    }
}

这里出现大量的判断比较,容易造成混乱,但主要记住:这些判断比较,在 ConcurrentHashMap 大部分是边界判断。记住这点后能够帮助理解大部分的判断比较,比如:sc == rs + MAX_RESIZERSsc == rs + 1实际上是对线程数的上下界的限制,超过限制,则不进入辅助转换。

transfer

ConcurrentHashMap 是分段进行并发转换,就是一个数组,按 幅度 划分,然后相应的线程获取到哪个分组,则负责该分组的转换的完成。那么重组转换的出口在哪里呢?只有当所有线程都执行完毕,处理转换的线程的信号量没有被获取了 ,才退出整个转换过程。默认最小幅度是 16,也就是说线程的最少处理元素个数是 16 个。

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    // stride 幅度
    int n = tab.length, stride;
    // 如果 CPU 大于 1,控制最少每个线程的处理量为 16 ==> n / 8 / NCPU
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range
    if (nextTab == null) {            // initiating
        try {
            // 数组翻倍,为什么要多出一个赋值操作?是因为 new 操作可能异常?貌似也不影响
            @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;
       }
       // 因为本方法的外层调用都使用了 CAS,所以可以保证此赋值的正确性(多线程情况下)
       nextTable = nextTab;
       // table 大小,最开始的转换范围是原数组大小
       transferIndex = n;
    }
    //...
 }

进入转换方法后,首先就是确定线程处理幅度,然后初始化 nextTable (如果需要的话),并初始化转换过程中需要用到的一些辅助属性,如:transferIndex = n = table.length

接下来,就是一个死循环(假象)。死循环内嵌死循环。第一个死循环使用到了局部参数 i 和 bound,实际上,在每个线程进入该方法后,都会获得自己这两个局部变量值,而它们的值变动则是在内部循环中开始赋值,一旦赋值成功,那么第一个死循环就变成了一个有界的 for 循环

优先看第二个内部循环, advance 变量控制了该循环。advance 变量主要表示:是否推进到下一个元素。它实际与 i 和 bound是有逻辑关系的,一旦 i 和 bound的关系不匹配,那么 advance 也就必须为 false,不再让线程进行推进,推进的操作是( --i )。也就是说,线程进入后,将有三个变量控制其运行,其中 bound, i是线程处理的数组边界,而 advance 则控制线程在这个边界中进行移动

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
  // ... 省略部分代码
    int nextn = nextTab.length;
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    // 是否推进到下一个元素,false 则表示还是处理当前元素
    boolean advance = true;
    boolean finishing = false; // to ensure sweep before committing nextTab
    for (int i = 0, bound = 0;;) {
        // f -> findNode;fh -> findNode hash
        Node<K,V> f; int fh;
        // 死循环主要是为了划分线程处理区间 !还有控制元素推进
        while (advance) {
            int nextIndex, nextBound;
            // 死循环标志位,不断死循环执行处理,没有太多意义,纯粹依靠标志位
            // 每一个线程进来,第一个判断都不成立
            // 通过 --i 来控制线程处理区间的推进,
            // 如果 --i > bound 说明区间范围超过线程的处理范围,线程不再该范围内就行推进,标志位为false
            // 每一次划分完,则 i 实际上是闭区间的尾部,而 bound 则为区间的首部,所以 --i 成功,进入区间下一个元素处理
            if (--i >= bound || finishing)
                advance = false;
            else if ((nextIndex = transferIndex) <= 0) {
                // 赋值 nextIndex
                // 小于0 :表已被划分完,不再作划分推进,跳出循环
                i = -1;
                advance = false;
            }
            else if (U.compareAndSetInt
                     (this, TRANSFERINDEX, nextIndex,
                      nextBound = (nextIndex > stride ?
                                   nextIndex - stride : 0))) {
                // CAS 替换值,将 transferIndex 更新为 transferIndex - stride
                // 控制此线程的处理区间为:bound ~ (nextIndex - 1)
                // 假定初始表大小为 35,2个线程进入(其实跟线程数无关,跟 CPU 有关),NCPU = 2 ,则幅度控制下为 16
                // 通过循环,划分下为:
                // 19 ~ 34
                // 3 ~ 18
                // 0 ~ 2
                // 也就说,transfer 的处理,(单线程)是从尾部到头部(当然总体情况下多线程则取决于线程的执行情况)
                bound = nextBound;
                i = nextIndex - 1;
                advance = false;
            }
        }
      // ... 省略部分代码
}

可以看到,内部死循环的主要作用,其实是为了划分分区(划分幅度为 stride,也可以意识到,即使是单线程,其执行也是按分区执行,并且执行的分区顺序是从尾部到首部。通过 CAS 保证分区的划分的线程安全,失败则重新循环再次操作。

划分完分区后,剩下的就是线程的处理过程。处理过程包括 2 部分,一部分是普通的元素处理,一部分是边界控制——退出出口

在每一个元素的处理过程中,线程都会先判断是否到达出口,是则退出?差不离,但退出包含两种情况,一种是普通的辅助线程的退出,它只擦自己的屁股,另外一种是整体线程的退出,它除了处理负责自己的退出出口,还要负责将重组后的结果 nextTable 重复赋值给 table,并为 sizeCtl 赋值为新数组大小的 0.75 倍的阈值

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
  // ... 省略部分代码
    // 如果 i < 0 || i >= n || i + n >= nextn ,都属于区间的边界判断
    // 超过边界则判断是否线程都已执行完毕,其实只有首尾区间的线程会触发到这个判断,
    // 其他的线程因为 stride < i < 2stride,所以不会触发此判断
    if (i < 0 || i >= n || i + n >= nextn) {
        int sc;
        // 扩容出口
        // 只有当 finishing 为 true 时,才真正将 nextTable 赋值给 旧 table 指针
        // 而 finishing 为 true 的唯一条件,是所有的线程都执行完毕
        if (finishing) {
            nextTable = null;
            table = nextTab;
            // 翻倍减去 0.25 ,为新数组大小的 0.75 倍的阈值
            sizeCtl = (n << 1) - (n >>> 1);
            return;
        }
        if (U.compareAndSetInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
            if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                return;
            // 只有当所有的线程都执行完毕,才能保证 finishing 为 true
            finishing = advance = true;
            i = n; // recheck before commit
        }
    }     
  // ... 省略部分代码
}

说完了边界出口,剩下的就是普通的操作了,有以下判断:

  • 当线程转换时旧数组对应位置上为 null,则直接 CAS 替换为 ForwardingNode(其hash = MOVED),表示转移过了;此时,当外部有操作 put 刚好命中此位置时,将会进入辅助转换的过程,判断依据就是 if (hash == MOVED)。也就是说,在重组转换过程中,进行 put 操作,将进入辅助转换过程。
  • 如果 hash 为 MOVED,则表示该位置已被其他线程转移过,推进到下一个元素

最后,进入与 HashMap 相同的链表重组和树结构重组的逻辑中,成功执行后,advance = true,继续推进处理元素(--i)。这里比 HashMap 多出一步,就是将旧数组对应位置上的标记为已处理。

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
  // ... 省略部分代码
    else if ((f = tabAt(tab, i)) == null)
        advance = casTabAt(tab, i, null, fwd);   // 如果旧表该位置为null,则标记为已处理
    else if ((fh = f.hash) == MOVED)         // 感觉不太可能遇到,毕竟线程单一负责自己的区域(?)
        advance = true; // already processed
    else {
        // 进入转换
        synchronized (f) {
            if (tabAt(tab, i) == f) {
                Node<K,V> ln, hn;
                // 普通链表的 hash 节点是正常的 hash 码,树节点的 hash 则默认小于 0
                // 重哈希算法与 HashMap 相同,都是以 2的n次幂 对应的二进制刚好为 1,
                // 直接移动高位部分元素
                if (fh >= 0) {
                // ... 省略部分代码
              }
                else if (f instanceof TreeBin) {
                  // ... 省略部分代码 
                setTabAt(nextTab, i, ln);
               setTabAt(nextTab, i + n, hn);
               // 处理完成后,将旧数组的节点标记为已处理(旧数据将没有数据)
               setTabAt(tab, i, fwd); 
                advance = true;
           }
          }
      }
  }
}

至此,整个 ConcurrentHashMap 的转换过程算完了

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/13121.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Spring七天速成[精简版]:入门必看(一)收藏起来

“天生我材必有用&#xff0c;千金散尽还复来&#xff01;” ----------持续更新Spring入门系列知识点------------- 你的点赞、关注、评论、是我创作的动力&#xff01; -------希望我的文章对你有所帮助-------- 前言&#xff1a;其实学习编程从来没有捷径&#xff0c;只有…

传奇架设gom引擎常见问题

传奇架设gom引擎常见问题 M2出现服务器启动异常&#xff01;&#xff01;&#xff01;An error occurred while attempting to initialize the Borland Database Engine 解决方法&#xff1a;解决方法:打开C盘删除PDOXUSRS.NET文件,重启电脑即可,如果无效请用下面这个方法 开…

数组:矩阵快速转置 矩阵相加 三元组顺序表/三元矩阵 随机生成稀疏矩阵 压缩矩阵【C语言,数据结构】(内含源代码)

目录 题目&#xff1a; 题目分析&#xff1a; 概要设计&#xff1a; 二维矩阵数据结构&#xff1a; 三元数组\三元顺序表顺序表结构&#xff1a; 详细设计&#xff1a; 三元矩阵相加&#xff1a; 三元矩阵快速转置&#xff1a; 调试分析&#xff1a; 用户手册&#xff…

“互联网寒冬”来袭,软件测试人员该如何度过这次危机?

互联网寒冬对测试人的影响 去年还在全网声讨互联网企业996呢&#xff0c;今年突然没声音了&#xff0c;也不用讨论在哪个路灯上吊死互联网资本家了&#xff0c;因为都被裁了。 继教育培训领域大幅度裁员之后&#xff0c;大厂裁员消息也开始陆续传出&#xff0c;百度AIG,MEG多…

Linux进阶-用户管理与文件权限

目录 用户和用户组 三个核心文件 /etc/passwd /etc/group /etc/shadow 文件权限 用户和用户组 用户&#xff1a;Linux系统的使用者。包括了管理员、系统用户和普通用户。 用户组&#xff1a;由一个用户或多个用户组成。用户与用户组关系可以为一对一、一对多、多对一、多…

从零开始搭建一个微服务项目(一)

文章目录Nacos搭建一. 安装nacos二.创建项目导入依赖三. 进行配置四.引入Feign远程调用五.引入RIbbon负载均衡六.Nacos配置中心Nacos搭建 一. 安装nacos 我安装的是window版&#xff0c;可参照该教程nacos安装教程 二.创建项目导入依赖 首先我们先创建一个主工程。 引入如下…

透明窗体和控件

调用函数设置窗体透明度&#xff1a; setWindowOpacity(x); x(0-1)可以为小数 0.1 0.2 0.3等 x0 时完全透明k1时不透明setWindowOpacity(0.5); 当有控件时&#xff0c;控件也变透明&#xff0c;在ui界面中添加两个按钮 使窗体透明但控件不透明 setWindowFlag&#xff08;Qt:…

【MQ工作队列模式】

1、模式介绍 ⚫Work Queues&#xff1a;与入门程序的简单模式相比&#xff0c;多了一个或一些消费端&#xff0c; 多个消费端共同消费同一个队列中的消息。 ⚫ 应用场景&#xff1a;对于任务过重或任务较多情况使用工作队列可以提高任务处 理的速度。 小结: 1、在一个队列中如果…

初学Nodejs(3):http模块

初学Nodejs http模块 1、概念 什么是客户端与服务端 在网络节点中&#xff0c;负责消费资源的电脑&#xff0c;叫做客户端&#xff1b;负责对外提供网络资源的电脑叫做服务器 http模块是Nodejs官方提供的、用来创建web服务器的模块。通过http模块提供的http.createServe()方…

[附源码]java毕业设计流浪动物救助系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

活动sql语句索引基本优化

前言 最近接到了一个需求开发&#xff0c;然后开发完成以后打算对sql进行一些优化&#xff0c;于是等所有功能开发完成以后对mapper文件里面的sql,和service层的查询语句都摘出来&#xff0c;然后设计了一些索引&#xff0c;下面就来说说一些大概的优化思路&#xff0c;至于mys…

WPF上位机通信组件与Modbus协议

1、Modbus通信方式与分类 - 串口 RS485&#xff08;一主多从&#xff09;&#xff1a;不同的报文格式&#xff1a;ModbusAscii&#xff08;ASCII字符方式进行发送&#xff09;、ModbusRTU&#xff08;Remote Terminal Unit&#xff09; - 以太网&#xff08;TCP点对点&#…

[博士后申请]套磁信的五大误区

博士后申请有一些技巧需要注意&#xff0c;下面就随知识人网一起来看看博士后申请套磁信的五大误区。 误区一&#xff1a;字数越多越好 Email字数控制在200字左右。教授每天处理上百封邮件&#xff0c;简单明了的邮件内容是为别人节约时间的一种礼貌;简短易回复的信件也会加大…

supervisor常见报错问题处理及使用教程

Supervisor 是用Python开发的一套通用的进程管理程序&#xff0c;能将一个普通的命令行进程变为后台daemon&#xff0c;并监控进程状态&#xff0c;异常退出时能自动重启。 官网介绍 Supervisor已经过测试&#xff0c;可以在Linux&#xff08;Ubuntu 9.10&#xff09;&#xf…

MySql常见复合查询(重点)

复合查询&#xff08;重点&#xff09; 多表查询 实际开发中往往数据来自不同的表&#xff0c;所以需要多表查询。本节我们用一个简单的公司管理系统&#xff0c;有三张表 EMP,DEPT,SALGRADE来演示如何进行多表查询。 显示雇员名、雇员工资以及所在部门的名字因为上面的数据来…

如何解决Web前端安全问题?

我国网络技术水平的提升&#xff0c;带动着WEB前端业务量的显著增长&#xff0c;人们对于网络服务的需求也日益复杂&#xff0c;与此同时&#xff0c;越来越多的黑客出现&#xff0c;其攻击水平也有了明显提升&#xff0c;WEB前端也成为了众多黑客进行网络攻击的主要目标。 因…

什么是零代码?零代码与低代码有什么联系与区别?

传统的软件研发方式目前并不能很好地满足企业的需求&#xff1a;人员成本高、研发时间长、运维复杂。 这时零代码或低代码工具出现在市面上并被关注就是必然趋势了。对于不太了解两者的人来说&#xff0c;零代码和低代码是什么&#xff1f;又有什么联系与区别&#xff1f; 01 …

uni小程序——评论、文本域、发送、键盘调起、有值后按钮变色等

一、简介 文本域默认显示一行&#xff0c;最多显示4行&#xff0c;到了4行之后不再增高。 输入值后按钮变色 二、案例演示 三、代码 <template><view><view class"plBox"><textarea auto-height"true" maxlength"-1" :s…

[Linux安装软件详解系列]04 安装Redis

目录1、查看服务器是否已安装Redis2、安装Redis1&#xff09;下载2&#xff09;解压3&#xff09;安装4&#xff09;移动配置文件到安装目录下5&#xff09;配置redis为后台启动6&#xff09;将redis-cli&#xff0c;redis-server拷贝到bin下7&#xff09;启动redis8&#xff0…

RabbitMQ简介及在Linux中安装部署(yum)

一、RabbitMQ简介及其作用 RabbitMQ简介 RabbitMQ是在2007 年发布&#xff0c;是一个在 AMQP(高级消息队列协议)基础上完成的&#xff0c;可复用的企业消息系统&#xff0c;是当前最主流的消息中间件之一。RabbitMQ是一个由erlang开发的AMQP&#xff08;Advanced Message Queu…