接下来就讲解put里面的三个方法,分别是

1、数组初始化方法initTable()

2、线程协助扩容方法helpTransfer()

3、计数方法addCount()

首先是数组初始化,再将源码之前,先得搞懂里面的一个重要参数,那就是sizeCtl。

sizeCtl默认为0,代表数组未初始化。

sizeCtl为正数,如果数组未初始化,那么其记录的是数组的初始容量,如果数组已经初始化,那么其记录的是数组的扩容阈值。

sizeCtl为-1,表示数组正在进行初始化。

sizeCtl小于0,并且不是-1,表示数组正在扩容,-(1+n),表示此时有n个线程共同完成数组的扩容操作。

接下来讲解initTable()方法

private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
    //第一次put的时候,table还没被初始化,所以能进入while。sizeCtl默认值是0,当有两个线程都想进行初始化时,线程A CAS成功,也就是else if为true,继续执行下面,而另一个线程cas就是false,就重新进行while循环,而这时sizeCtl为-1.所以这个线程就
    放弃cpu的控制权,说白了就是在多线程下保证初始化只执行一次。
while ((tab = table) == null || tab.length == 0) {//tab在这里赋值,是table的引用
if ((sc = sizeCtl) < 0)//sc在这里赋值。是sizeCtl的引用。
Thread.yield(); // lost initialization race; just spin//线程放弃cpu的控制权。
        //SIZECTL:表示当前对象的内存偏移量,sc表示期望值,-1表示要替换的值,设定为-1表示要初始化表了
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {//SIZECTL是地址偏移量,如果SIZECTL对应地址的值与sc相等,说明当前的线程是第一个到达这条语句的线程,那么就会将SIZECTL地址所对应的值替换成-1,而SIZECTL地址偏移量对应的对象就是sizeCtl
try {
if ((tab = table) == null || tab.length == 0) {//再次进行判断,防止在进行U.compareAndSwapInt(this, SIZECTL, sc, -1)的时候有其他线程并发进入方法,导致出错,使用双重锁
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;//sc在前边就赋值了,如果有初值,那么这里n就是设定的初始值,否则n就是默认容量。16
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];//基于初始长度,构建数组对象
table = tab = nt;
sc = n - (n >>> 2);//这里就是计算扩容阈值,并赋值给sc。也就是n-0.25n = 0.75n
}
} finally {
sizeCtl = sc;//将扩容阈值,赋值给sizeCtl,第一次初始化后。
}
break;
}
}
return tab;
}

所以在这个方法中,sizeCtl为正数,如果数组未初始化,那么其记录的是数组的初始容量,如果数组已经初始化,那么其记录的是数组的扩容阈值。就是这个含义。

第二个要讲解的方法是addCount方法。

这段代码分为两个部分,一个是计数部分,另一个是扩容部分。

<用两种方法进行计数,一个是用cas对baseCount<baseCount是一个全局属性,volatile的>进行加法计数,另一个是用CounterCell数组,其实CounterCell对象就有一个属性是value,用CounterCell构建一个数组,然后哪个线程要进行加法,就用这个线程产生一个hash值,并用这个数与CounterCell数组的长度减一做与运算,得到的结果就是CounterCell数组的index,然后对这个index对应的CounterCell对象的value做加法。在最后统计计数的时候是用:baseCount+每个CounterCell的value>
countcell就是通过分散计算来减少竞争。其内部有一个基础值和一个哈希表。当没有竞争的时候在基础值上计数。有竞争的时候通过哈希表计数(每个线程有一个哈希值,通过哈希值确定在哈希表的位置,在这个位置进行计数,不同位置互不影响)

计数部分:通过baseCount和CounterCell数组,二选一的参与计数
扩容部分:当大于扩容阈值的时候进行扩容,当满足扩容条件时才能扩容
如果有别的线程正在进行扩容,那么就存在nextTable(一个全局属性,表示正在扩容的线程),就把nextTable作为参数传入transfer方法,就是让这个线程帮忙扩容nextTable
如果没有别的线程正在扩容,那么就把null传入transfer方法,让transfer方法创建一个nextTable

private final void addCount(long x, int check) {
CounterCell[] as; long b, s;//as表示counterCells引用, b表示获取的baseCount值, s应该是表示元素个数。
 //当CounterCell数组不为空,则优先利用数组中的CounterCell记录数量
    //或者当baseCount的累加操作失败,会利用数组中的CounterCell记录数量
    //条件一:as!=null true:表示counterCells以及初始化过了,当前线程应该将数据写入到对应的counterCell中。
            //as==null 也就是条件1为false,那么表示counterCells未初始化,当前所有线程应该将数据写到baseCount中。
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {//条件二:true:表示当前线程cas替换数据成功  false表示发生竞争了,可能需要重试或者扩容。//因为这里有个!,所以应该false才能进入
    //什么时候会进入到if判断里面
         //1.条件一:as!=null true:表示counterCells以及初始化过了,当前线程应该将数据写入到对应的counterCell中。
        //2.条件二:false表示发生竞争了,可能需要重试或者扩容
CounterCell a; long v; int m;//a表示当前线程命中的CounterCell单元格 v表示期望值,m表示as数组的长度。
boolean uncontended = true; //标识是否有多线程竞争 ,//true表示未发生竞争,false表示发生竞争。
      //当as数组为空
        //或者当as长度为0
        //或者当前线程对应的as数组桶位的元素为空
        //或者当前线程对应的as数组桶位不为空,但是累加失败
        //条件一:as==null 为true,说明counterCells未初始化,那么上面就是根据多线程写base发生竞争进入到这里的。
                //as!=null为false,说明counterCells已经初始化了。当前线程应该是找自己的counterCells写值。
if (as == null || (m = as.length - 1) < 0 ||
        //ThreadLocalRandom.getProbe()表示当前线程的hash值。 m是CountCell长度-1,as.length一定是2的次方数,比如长度为16,那么减一就是15
            //也就是b1111,那么与上之后肯定是小于等于当前长度的值,也就是下标。
            //如果a==null,也就是条件为true,说明当前线程对应下标的CountCell为空,那么就需要创建
            //如果是false,不为空,说明下一步想要将x的值添加到CountCell中。
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
         //条件三:true:表示cas失败,意味着当前线程对应的CountCell有竞争,
                    //false,表示cas成功
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
       //都有哪些情况会调用下面的方法:
            //1.as==null 为true,说明counterCells未初始化,那么上面就是根据多线程写base发生竞争进入到这里的,那么初始化。
            //2.如果a==null,也就是条件为true,说明当前线程对应下标的CountCell为空,那么就需要创建
            //3.true:表示cas失败,意味着当前线程对应的CountCell有竞争,那么就可能是重试或者扩容。
            //以上任何一种情况成立,都会进入该方法,传入的uncontended是false
fullAddCount(x, uncontended);//也就是countCells是一个全局得volatile的CountCell数组,创建实在fullAddCount方法中。
return;
}
if (check <= 1)
return;
s = sumCount();//计算元素个数
}
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
       //当元素个数达到扩容阈值
        //并且数组不为空
        //并且数组长度小于限定的最大值
        //满足以上所有条件,执行扩容
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
if (sc < 0) { //sc小于0,说明有线程正在扩容,那么会协助扩容
      //扩容结束或者扩容线程数达到最大值或者扩容后的数组为null或者没有更多的桶位需要转移,结束操作
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
            //扩容线程加1,成功后,进行协助扩容操作
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);//协助扩容,newTable不为null
}
          //否则sc>=0,说明是首个扩容的线程,所以transfer传入的参数是null
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}

接下来就是讲解上面的fullAddCount方法

① 当CounterCell数组不为空,优先对CounterCell数组中的CounterCell的value累加

② 当CounterCell数组为空,会去创建CounterCell数组,默认长度为2,并对数组中的CounterCell的value累加

③ 当数组为空,并且此时有别的线程正在创建数组,那么尝试对baseCount做累加,成功即返回,否则自旋

private final void fullAddCount(long x, boolean wasUncontended) {//wasUncontended只有counterCells初始化之后,并且当前线程竞争修改失败,才会是false。其余情况都是true。
int h;//h表示线程的hash值。
    //获取当前线程的hash值
if ((h = ThreadLocalRandom.getProbe()) == 0) {//条件成立:说明当前线程还未分配hash值。
ThreadLocalRandom.localInit();//给当前线程分配hash值 // force initialization
h = ThreadLocalRandom.getProbe();//取出当前线程的hash值,赋值给h
wasUncontended = true;//为啥这里强制设为true,当前线程肯定是写入到了countCells[0]位置,不把它当作一次真正的竞争。
}
boolean collide = false; //标识是否有冲突,如果最后一个桶不是null,那么为true //表示扩容意向 false一定不会扩容,true可能会扩容  // True if last slot nonempty
for (;;) {//自旋
CounterCell[] as; CounterCell a; int n; long v;//as表示counterCells引用,a表示当前线程命中的CounterCell,n表示counterCells数组长度,v表示期望值
       //数组不为空,优先对数组中CouterCell的value累加
        //CASE1:表示countCells已经初始化了,当前线程应该将数据写入到对应的CounterCell中。
if ((as = counterCells) != null && (n = as.length) > 0) {
      //2.如果a==null,也就是条件为true,说明当前线程对应下标的CountCell为空,那么就需要创建
            //3.true:表示cas失败,意味着当前线程对应的CountCell有竞争,那么就可能是重试或者扩容。这两种情况会进入到这个if判断中
            //线程对应的桶位为null
         //CASE1.1:a = as[(n - 1) & h]) == null true表示当前线程对应的下标位置的CounterCell为null,需要创建new CounterCell
if ((a = as[(n - 1) & h]) == null) {
if (cellsBusy == 0) { //true:表示当前锁未被占用,false表示锁被占用 // Try to attach new Cell
CounterCell r = new CounterCell(x); // Optimistic create//创建CounterCell对象
                //利用CAS修改cellBusy状态为1,成功则将刚才创建的CounterCell对象放入数组中
                    //条件一:true:表示当前锁未被占用,false表示锁被占用
                    //条件二:true:表示当前线程获取锁成功,false表示当前线程获取锁失败。
if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean created = false;//是否创建成功的标记
try { // Recheck under lock
CounterCell[] rs; int m, j;//rs表示当前countCells引用,m表示rs的长度,j表示当前线程命中的下标。
                  //桶位为空, 将CounterCell对象放入数组
                            //条件一条件二恒成立
                            // rs[j = (m - 1) & h] == null为了防止其它线程初始化过该位置,然后当前线程再次初始化该位置,导致数据丢失。
if ((rs = counterCells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
rs[j] = r;
created = true;//表示放入成功
}
} finally {
cellsBusy = 0;
}
if (created)//成功退出循环
break;
continue; //桶位已经被别的线程放置了已给CounterCell对象,继续循环 // Slot is now non-empty
}
}
collide = false;//将扩容意向改为false,原因是因为再CASE1.1中当前CounterCell都是为null,不可能不让写。因此不需要扩容。
}
          //桶位不为空,重新计算线程hash值,然后继续循环
            //CASE1.2:只有一种情况会来到这里,wasUncontended只有counterCells初始化之后,并且当前线程竞争修改失败,才会是false
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
        //重新计算了hash值后,对应的桶位依然不为空,对value累加
            //成功则结束循环
            //失败则继续下面判断
            //CASE1.3:当前线程rehash过hash值,然后新命中的CounterCell不为空。则来到这里。
            //true:写成功,退出循环,
            //false:表示rehash之后命中的新的cell也有竞争,重试1次  再重试一次
else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
break;
         //数组被别的线程改变了,或者数组长度超过了可用cpu大小,重新计算线程hash值,否则继续下一个判断
            //CASE1.4:条件一:n >= NCPU true->表示扩容意向改为false,表示不扩容了。false说明counterCells数组还可以扩容
            //条件二:counterCells != as true表示其它线程已经扩容过了,当前线程rehash之后重试即可。
else if (counterCells != as || n >= NCPU)
collide = false; // At max size or stale
        //当没有冲突,修改为有冲突,并重新计算线程hash,继续循环
            //CASE1.5  !collide=true,设置扩容意向为true,但是不一定真的发生扩容。因为一旦进入这里,那么又会rehash一下,又会重来。
else if (!collide)
collide = true;
         //如果CounterCell的数组长度没有超过cpu核数,对数组进行两倍扩容
            //并继续循环
            //CASE1.6:真正扩容的逻辑
            //条件一:cellsBusy == 0 true表示当前无锁状态,当前线程可以去竞争这把锁。
            //条件二:U.compareAndSwapInt(this, CELLSBUSY, 0, 1)  true表示当前线程获取锁成功,可以执行扩容逻辑。
                                                            //false表示当前时刻有其它线程正在做扩容相关的操作。
else if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
try {
if (counterCells == as) {// Expand table unless stale //这里又有双重检测,就是为了防止扩容过了又再次扩容。
CounterCell[] rs = new CounterCell[n << 1];//扩容为两倍扩容
for (int i = 0; i < n; ++i)
rs[i] = as[i];//将旧数组的值放到新数组。
counterCells = rs;
}
} finally {
cellsBusy = 0; //释放锁
}
collide = false;
continue; // Retry with expanded table
}
h = ThreadLocalRandom.advanceProbe(h);//重置当前hash值 rehash
}
     //CounterCells数组为空,并且没有线程在创建数组,修改标记,并创建数组,因为前面CASE1是数组不为空,所以这里是数组为空
        //CASE2:当前条件countCells还未初始化,as为null
        //条件一:cellsBusy == 0  true表示当前未加锁
        //条件二:counterCells == as,为什么又重新比较一遍,明明前面CASE1已经赋值未null了。因为其它线程可能会在你给as赋值之后修改了counterCells。
        //条件三:如果未true,表示获取锁成功,会把cellsBusy改成1。false表示其它线程正在持有这把锁,那么当前线程就进不了这里面了。
else if (cellsBusy == 0 && counterCells == as &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean init = false;
try { // Initialize table
if (counterCells == as) {//为什么这里又判断了一下counterCells == as,防止其它线程已经初始化了,当前线程再次初始化,就会覆盖掉其它线程初始化的CountCell,导致丢失数据。
CounterCell[] rs = new CounterCell[2];//初始容量为2
rs[h & 1] = new CounterCell(x);
counterCells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
    //CASE3:
        //1.当前cellsBusy加锁状态,表示其他线程正在初始化countCells,所以当前线程将值累加到baseCount。
        //2.countCells被其它线程初始化后,当前线程需要将数据累加到base。
        //数组为空,并且有别的线程在创建数组,那么尝试对baseCount做累加,成功就退出循环,失败就继续循环
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
break; // Fall back on using base
}
}

总结:addCount方法通过属性baseCount 和 counterCell数组中所有的元素的和来记录size
在无竞争的情况下,通过cas当前map对象的baseCount为baseCount + 1,
有竞争情况下,上诉cas失败,会初始化一个长度为2的CounterCell数组,数组会扩容,每次扩容成两倍,每个线程有在counterCell数组中对应的位置(多个线程可能会对应同一个位置), 如果位置上的CounterCell元素为空,就生成一个value为1的元素,如果不为空,则cas当前CounterCell元素的value为value + 1;如果都失败尝试cas当前map对象的baseCount为baseCount + 1。

ConcurrentHashMap源码解读二的更多相关文章

  1. jQuery.Callbacks 源码解读二

    一.参数标记 /* * once: 确保回调列表仅只fire一次 * unique: 在执行add操作中,确保回调列表中不存在重复的回调 * stopOnFalse: 当执行回调返回值为false,则 ...

  2. HashTable、HashMap与ConCurrentHashMap源码解读

    HashMap 的数据结构 ​ hashMap 初始的数据结构如下图所示,内部维护一个数组,然后数组上维护一个单链表,有个形象的比喻就是想挂钩一样,数组脚标一样的,一个一个的节点往下挂. ​ 我们可以 ...

  3. (转)go语言nsq源码解读二 nsqlookupd、nsqd与nsqadmin

    转自:http://www.baiyuxiong.com/?p=886 ---------------------------------------------------------------- ...

  4. ConcurrentHashMap源码解读一

    最近在学习并发map的源码,如果由错误欢迎指出.这仅供我自己学习记录使用. 首先就先来说一下几个全局变量 private static final int MAXIMUM_CAPACITY = 1 & ...

  5. mybatis源码解读(二)——构建Configuration对象

    Configuration 对象保存了所有mybatis的配置信息,主要包括: ①. mybatis-configuration.xml 基础配置文件 ②. mapper.xml 映射器配置文件 1. ...

  6. go语言nsq源码解读二 nsqlookupd、nsqd与nsqadmin

    nsqlookupd: 官方文档解释见:http://bitly.github.io/nsq/components/nsqlookupd.html 用官方话来讲是:nsqlookupd管理拓扑信息,客 ...

  7. vue2.0 源码解读(二)

    小伞最近比较忙,阅读源码的速度越来越慢了 最近和朋友交流的时候,发现他们对于源码的目录结构都不是很清楚 红色圈子内是我们需要关心的地方 compiler  模板编译部分 core 核心实现部分 ent ...

  8. ROS源码解读(二)--全局路径规划

    博客转载自:https://blog.csdn.net/xmy306538517/article/details/79032324 ROS中,机器人全局路径规划默认使用的是navfn包 ,move_b ...

  9. Python Web Flask源码解读(二)——路由原理

    关于我 一个有思想的程序猿,终身学习实践者,目前在一个创业团队任team lead,技术栈涉及Android.Python.Java和Go,这个也是我们团队的主要技术栈. Github:https:/ ...

随机推荐

  1. MySQL入门(6)——流程控制

    MySQL入门(6)--流程控制 IF语句 条件判断语句,逻辑与大多数编程语言相同,表示形式如下: IF condition THEN ... [ELSE condition THEN] ... [E ...

  2. HDU_5414 CRB and String 【字符串】

    一.题目 CRB and String 二.分析 对于这题,读懂题意非常重要. 题目的意思是在$s$的基础上,按题目中所描述的步骤,即在$s$中任意选择一个字符$c$,在这个字符后面添加一个不等于$c ...

  3. vim宏录制的操作

    1:在vim编辑器normal模式下输入qa(其中a为vim的寄存器) 2:此时在按i进入插入模式,vim编辑器下方则会出现正在录制字样,此时便可以开始操作. 3:需要录制的操作完成后,在normal ...

  4. mysql连接不上本地服务器或者localhost:3306报错

    今天初学MySQL数据库就遇到问题: 主要是本地服务器登录问题 workbench里双击那个connection出现的 解决方法: 1:看一看防火墙,这是最常见的,这种主要是防火墙限制了访问,可能是安 ...

  5. [Python] 波士顿房价的7种模型(线性拟合、二次多项式、Ridge、Lasso、SVM、决策树、随机森林)的训练效果对比

    目录 1. 载入数据 列解释Columns: 2. 数据分析 2.1 预处理 2.2 可视化 3. 训练模型 3.1 线性拟合 3.2 多项式回归(二次) 3.3 脊回归(Ridge Regressi ...

  6. Hashtable 渐渐被人们遗忘了,只有面试官还记得,感动

    尽人事,听天命.博主东南大学硕士在读,热爱健身和篮球,乐于分享技术相关的所见所得,关注公众号 @ 飞天小牛肉,第一时间获取文章更新,成长的路上我们一起进步 本文已收录于 「CS-Wiki」Gitee ...

  7. FFMPEG编译问题记录

    一.ffmpeg下载与配置 下载地址 FFmpeg/FFmpeg (https://github.com/FFmpeg/FFmpeg) ~$ git clone https://github.com/ ...

  8. Dynamics CRM报表无法访问提示“报表服务器无法访问或使用加密密钥。你可能需要将服务器添加到扩展组,或重新导入”

    当我们部署Dynamics CRM的环境的时候如果报表配置的不规范会出现很多问题,尤其是这个问题相对来说更棘手,解决起来非常麻烦. 网上很多教程都说直接到报表配置页删除密钥就可以了,实际上删除的时候会 ...

  9. Webpack的基本配置和打包与介绍(二)

    1. 前言 在上一章中我们学习到了webpack的基本安装配置和打包,我们这一章来学学如何使用loader和plugins 如果没看第一章的这里有传送门 2. Loader 2.1 什么是loader ...

  10. HarmonyOS开发者看过来,HDD上海站传递的重要信息都在这里

    4月17日,颇有HarmonyOS年度总结性质的HarmonyOS开发者日活动上海站正式开始. 活动中,华为消费者业务AI与智慧全场景业务部副总裁段孟对HarmonyOS生态建设的最新进展做了发言,并 ...