接下来就讲解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. 键盘--扫描码--ASCII码--显示器上的字符

    在上一篇,我讲了键盘操作会产生扫描码以及如何解析Pause键和Print Screen键的扫描码. 在这一篇,我会说清楚"键盘上的输入为什么会出现在显示器上". 极简版 我们敲击键 ...

  2. P4847 银河英雄传说V2 题解(Splay)

    题目链接 P4847 银河英雄传说V2 解题思路 我天哪!!!\(splay\)在\(rotate\)的时候先\(upd(y)\)再\(upd(x)\)!!以后不能再因为这个\(WA\)一晚上了!!! ...

  3. 冒泡排序算法的实现(Java)

    什么是冒泡排序 冒泡排序(Bubble Sort),是一种计算机科学领域的较简单的排序算法.它重复地走访过要排序的元素列,依次比较两个相邻的元素,如果顺序(如从大到小.首字母从Z到A)错误就把他们交换 ...

  4. 【LeetCode】2020-04 每日一题

    8. 字符串转换整数 (atoi)(中等) [分类]:模拟.正则表达式 [题解]: 解法1:直接模拟,但是在判断INT_MAX和INT_MIN上需要注意,因为直接判断会超出范围,所以可以将式子转换一下 ...

  5. 攻防世界 reverse SignIn

    SignIn    2019_SUCTF __int64 __fastcall main(__int64 a1, char **a2, char **a3) { char mod; // [rsp+0 ...

  6. 攻防世界 reverser secret-galaxy-300

    secret-galaxy-300 school-ctf-winter-2015 运行程序 完全没有flag的身影呀 ida查看字符串 也没有相关信息 动态调试,看运行后内存信息 发现了一串字符 al ...

  7. matplotlib常规使用方法

    1,指定图片大小和像素 Python绘图问题:Matplotlib中指定图片大小和像素 2,绘图命令的基本架构及其属性设置 绘图与可视化 3,python基础语法(二)--- plt的一些函数使用 p ...

  8. PAT (Basic Level) Practice (中文) 1050 螺旋矩阵 (25 分) 凌宸1642

    PAT (Basic Level) Practice (中文) 1050 螺旋矩阵 (25 分) 目录 PAT (Basic Level) Practice (中文) 1050 螺旋矩阵 (25 分) ...

  9. oo第四单元暨课程总结

    第四单元架构设计总结 第一次作业 单独写了MyUmlClass.MyUmlInterface.MyUmlOperation三个类对应UML中相应元素,在UML图中这几个元素包含一些下级元素,如Clas ...

  10. OOUnit4Summary

    一.前三次作业的架构设计 前言 第四单元以uml建模语言为背景,让我们通过已有的模型架构,设计实现类图(顺序图,状态图)解析器:在理解uml整体架构体系的基础上,读懂并熟练运用官方的给的代码,设计有层 ...