0
点赞
收藏
分享

微信扫一扫

juc源码解读四(ConcurrentHashMap)

爱动漫建模 2022-04-04 阅读 53
jucjava

put方法

自旋方法;

initTable方法

initTable方法是个while循环,退出条件是数组初始化好了;返回值是初始化好的数组;
sizeCtl最开始的值
将sizeCtl赋值给了一个临时变量sc,两个作用
1)cas操作设置sizeCtl为-1前先判断sizeCtl是否还和sc相等,相等则cas操作可以成功,则可以初始化数组,即以临时变量作为比较依据;
2)存储sizeCtl被修改为-1之前的值,便于在将sizeCtl修改为-1后,判断第一个进入initTable方法的线程sizeCtl最初的大小;sizeCtl最初的大小与用户调用chm构造方法的方式有关,如果用户用的是chm的默认构造函数,则sizeCtl为0,后续将以16作为初始化数组的容量;若用的是有参构造,传入了chm数组“初始化容量x”,则此时在构造函数中会将sizeCtl赋值为不小于x+x/2+1的最小的2的次方数y,此值y才是真正的初始化数组容量;
初始化过程
假设最开始线程A,B同时进入initTable方法,此时的sizeCtl的值只有两种可能,为0或者为y(2的幂),当线程A,B同时进行cas修改sizeCtl为-1,若A线程修改成功,则线程B再次自旋,感知到sizeCtl为-1,则会执行Thread.yield()让出cpu时间片,进入就绪状态,下一刻线程B可能会重新抢到cpu执行权也可能没抢到。若没抢到,则线程A此时会根据sizeCtl初始化数组,随后将sizeCtl的值更新为0.75n(为下次扩容的阈值,达到该值就扩容),n为数组长度,最后退出循环;线程B再次拿到cpu执行权时,发现数组已经初始化好了,最后也退出;

判断(n-1)&hash(key)所在桶位头节点状态

一共有四种情况,如下
头节点为null
cas给该桶位添加一个头节点,cas失败则直接break退出put自旋;

存在头节点且其hash为-1
表示当前桶位是FWD节点,接着当前线程执行helpTransfer方法,协助table迁移数据至新的更大table;

存在头节点且其hash不小于0
此时该桶位处为链表结构,从头节点开始遍历链表,比较插入节点和每个节点的key是否相等;首先会执行synchronized(f),f代表头节点,所以chm的并发是全局并发,局部同步;其实这里遍历链表,只需要比较key是否相等,比较key的hash是否相等没意义,因为此时不管key.hash是否相等,key.hash&(n-1)一定相等,hash只是起路由作用,已经知道路由到了当前链表的桶位,又回头去和链表上的每个元素比较key.hash就没意义了;
1)若key相等,则属于冲突了,则用插入value替换旧的value;
2)若key不相等,则找到队尾节点,加在后边,结束遍历;
存在头节点且为TreeBin类型
红黑树代理节点

addCount

进入该方法,sizeCtl只有两种情况,一是负数,表示正在扩容,高16位表示扩容戳,低16位表示扩容线程数量加1;二是正数,表示扩容阈值,是当前数组长度的0.75倍;

1)统计当前table一共有多少数据

1.1)竞争不激烈时,直接存储在long型的baseCount中;
1.2)ThreadLocalRandom.getProbe() & (counterCells.length – 1),通过上述取&找到CounterCells类型数组中的某个桶位进行存储;底层是基于LongAddr;
1.3)执行sumCount方法,对counterCells数组中的每个桶位求和,得到chm中元素总个数;

2)判断是否达到扩容阈值的标准,触发扩容

这是个while循环,循环条件是,元素总个数大于sizeCtl,当sizeCtl为负数,肯定成立,当sizeCtl为正数,此时若元素个数大于它,则表示当前table需要扩容;
但此时有两种情况,即一是sizeCtl为负数,二是sizeCtl为正数,为负数时,则表示正在扩容,所以在协助扩容前先判断扩容是否结束,没结束,则调用transfer(tab,nextTable)方法协助扩容;为正数时,则代表扩容阈值,此时当前线程为此次扩容的第一个线程,调用transfer(tab,null)方法,准备扩容;

2.1)先获取扩容唯一标示戳;

执行resizeStamp方法,入参为当前table长度,求出n的左边0的个数,或上1左移15位得到扩容戳rs,用于标示本次扩容;

2.2)若sizeCtl为负数

判断若sizeCtl为负数,则进一步判断扩容是否结束,没结束,则cas修改sizeCtl的值加1,表示增加了一个协助扩容的线程,准备执行transfer方法,其第二个参数为nextTable的值;
判断扩容已经结束一共5个条件,其中条件2和条件3有bug,不过bug已提交;只要有一个条件成立,则代表扩容已结束,当前线程退出循环;5个条件分别是:
a)扩容戳是否变了,将sizeCtl右移16位与rs比较,不相等,表示当前线程获取的扩容戳不是本次扩容的;(rs为计算得到的扩容戳)
b)sc与rs+1是否相等;
应该将rs左移16位再加1,当扩容线程数为0个时,sc的低16位等于rs+1,此时都没有线程参与扩容,则扩容已结束;sc是sizeCtl,rs为扩容戳;
c)sc与rs+65535是否相等;
应该将rs左移16位再加65535,再判断;这次判断的当前线程是否超过允许参与扩容线程数目的上限;
d)nextTable是否指向null;
e)transferIndex是否小于或等于0;
若以上五个条件都不满足则表示扩容未结束,则cas修改sizeCtl加1,调用扩容方法transfer(tab,nt)协助扩容;
sizeCtl为负数时,判断扩容是否结束,总结一下5个条件满足一个即代表扩容已结束,5个条件依次是:sizeCtl的高16位的扩容戳不是本此扩容的,sizeCtl的低16位线程数小于1,或者大于最大值65535,nextTable为null,transferIndex不大于0;

2.3)若sizeCtl为正数

判断若sizeCtl大于0,则表示当前线程可以触发扩容,接着cas修改sizeCtl左移16位,再加2,其中高16位表示扩容戳,低16位表示扩容线程数量加1,修改成功,则表示当前线程是触发扩容的第一个线程,此时sizeCtl变成了负数,准备执行扩容方法transfer,其第二个参数为null;

2.4)再次执行一次sumCount方法,更新元素总个数;

判断数据是否迁移的依据

k.hash&(table.length-1)得到在数组的位置,数组长度是2的幂,(16-1)为15,二进制为0b1111,(32-1)为31,二进制为0b11111,十进制数31比十进制数15的第五位多了一个1,所以只需要判断k.hash的第五位是否为0,为0则31多出来的1不起作用,数据还是原桶位。所以直接比较k.hash& oldtable.length是否为0,因为16的二进制为10000,可以检测出table.length的第五位是否为0。

transfer方法执行扩容

分配扩容步长stride

最小为16;

检查第二个入参是否为null

为null表示触发扩容的线程,则会新建一个长度为原来一倍的目标表保存至属性中,方便其他线程调用;
设置transferIndex初始值为旧表长度,可见迁移数据是从索引最大的位置开始的,每次分配完一段区间后,将该值更新,当该值小于或等于0表示没有区间可分配了;transferIndex管理分配进度,表示迁移数据整体位置的一个标记;

自旋

a)i
表示当前正在迁移哪个索引桶位,初始值为0,分配任务后为transferIndex-1;
b)bound
表示分配下界,初始值为0,分配任务后为transferIndex-stride,当i<bound说明分配给当前线程的迁移任务已完成,可以再次去申请分配新的任务区间;
自旋主要做三件事:
1)给当前线程分配任务区间,[transferIndex-stride,transferIndex-1];
2)维护当前线程处理任务进度(–i控制);
3)维护全局数组全局分配进度(transferIndex控制)
在这里插入图片描述

lastRun机制局限性

引入lastRun机制,是为了处理连续的高位数据,减少后续遍历,如低1——>高2——>高3——>高4——>高5——>高6,此时lastRun指向高2——>高3——>高4——>高5——>高6,后边遍历构建链表时,只需遍历一轮,就可退出循环,因为第二轮时发现p和lastrun相等,都指向了高2,所以会退出循环,最终得到ln指向低1,hn指向高2——>高3——>高4——>高5——>高6。
但若是对于非连续的如低1——>高2——>低3——>高4——>低5——>高6,则lastRun机制不起作用了,因为此时lastRun是指向高6的,接着再次遍历链表时,仍然需要判断k.hash& oldtable.length是否为0,为0意味着不迁移,我们称之为低位节点,用ln表示,执行new Node方法赋值给ln,构造方法的最后一个入参是ln,表示当前node的next节点,所以属于头插法,最后ln指向低5——>低3——>低1;
同理,若key的hash和旧表长度相与后的结果不为0,意味着迁移,我们称之为高位节点,用hn表示,执行new Node方法复制给hn,构造方法的最后一个入参是hn,表示当前node的next节点,属于头插法,最后hn指向高4——>高2——>高6,为啥不是 高6——>高4——>高2,因为遍历前hn的初始值为高6,而ln初始值为null;

CopyOnWriteArrayList

https://www.cnblogs.com/chengxiao/p/6881974.html

举报

相关推荐

0 条评论