ConcurrentHashMap支持多线程并发,但又不像Hashtable和Vector一样简单粗暴的加上synchronized关键字来完成,源码大量使用了cas来保证操作的原子性,效率比Hashtable和Vector要高,也是目前多线程开发中用的最多的map<K,V>类集合。
阅读本专栏,需要读者对多线程开发有一定的理解,并且要理解ConcurrentHashMap底层数组+链表+红黑树的数据结构。了解HashMap源码,可以帮助理解该专栏的内容。
该篇文章着重分析ConcurrentHashMap的计数方法。
ConcurrentHashMap不同于HashMap,没有size属性,想要知道ConcurrentHashMap的大小,需要每次经过计算获取。
为支持多线程并发调用,ConcurrentHashMap通过一个CountCell数组来为节点计数(类似LongAdder),数组各元素之和加上baseCount即为ConcurrentHashMap的大小。
下面通过计数方法详细说明:
addCount计数
//ConcurrentHashMap 计数
//ConcurrentHashMap没有size属性,size()方法通过计算baseCount和CounterCell[]中的值进行加和得到。
//多个线程对size进行加减操作时会从baseCount和CounterCell[]竞争一个进行加1,不管加在何处,对总数是不影响的
//参考LongAdder的思想
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
//(as = counterCells) != null 当前ConcurrentHashMap的conterCells复制个as,并判断是否为空
//!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)
//如果为空则执行第二个条件对使用cas的方式对baseCount加上x,cas成功则执行完成。
//可以理解为,先判断counterCells是否为空,如果为空则当前线程去竞争baseCount进行加x,如果不成功则进入到if代码对counterCells进行操作
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
//根据以上条件可知,进入到该分支时,要么counterCells不为空,要么counterCells为空,但竞争baseCount失败。
CounterCell a; long v; int m;
boolean uncontended = true;
//进一步判断,
// 1、前两个条件一起看,as(即counterCells)为空(第一个条件,as == null),或者as.length 为0 (第二个条件,(m = as.length - 1) < 0,此处有一个as.length-1赋值给m的操作),则直接调用fullAddCount,里面有初始化并加x的操作。
// 2、如果as不为空(前两个条件,其等价条件为as!=null && as.length > 0),则执行第三个条件,
// (a = as[ThreadLocalRandom.getProbe() & m]) == null),判断当前线程对应的counterCells数组中下标未知的元素是否为空,
// counterCells是个数组,数组的元素是CounterCell,每个线程使用具体数组的哪个元素去完成计数,
// 是通过线程探针ThreadLocalRandom.getProbe() & m去计算,ThreadLocalRandom.getProbe()可以认为是线程内部存储的一个不变的随机数。当发生线程争用同一个数组元素时,可以通过ThreadLocalRandom.advanceProbe去更新探针的值(此处并未用到)
// 这个条件的含义就是,看一下当前线程对应的counterCells里的元素,是否为空,并把这个元素赋值给a
// 3、如果为空则进入if走fullAddCount,fullAddCount会对元素进行初始化,如果不为空则看第四个条件(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
// 通过cas的方式对a.value进行加x,如果执行成功则说明加1成功,不再走进if,失败则走进if调用fullAddCount(),并把执行结果赋给uncontended,此时为(false)
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
//调用fullCount,并传入uncountended,这个变量用来标识是否最后一个条件执行失败进入的fullAddCount。
//如果为false说明是第四个条件失败后进入的。
fullAddCount(x, uncontended);
return;
}
//走到这说明加完了,根据传入的check,判断下一步是否扩容,check <= 1则直接返回
if (check <= 1)
return;
//计算size
s = sumCount();
}
//传入的check>=0,则校验是否需要扩容
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
//看一下这三个条件:
// 1、s >= (long)(sc = sizeCtl),元素总数已经大于扩容因子了(sizeCtl),同时这里有个赋值,将sizeCtl赋值给sc。
// 2、(tab = table) != null 当前table不为空,如果为空应该走初始化,而不是扩容。也有个赋值tab = table。
// 3、数组长度尚没有达到上限MAXIMUM_CAPACITY。还是有个赋值操作 n = tab.length。
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
//产生当前数组扩容的标识戳,根据数组的长度n计算,具体看resizeStamp方法
int rs = resizeStamp(n);
//第一次有线程进来扩容时,sc=sizeCtl为扩容阈值,肯定是大于0的,因此不会走这里
//后续再有线程进来扩容,测试的sc经过下面else if 的计算,已经是负数了,因此会走进这个if
if (sc < 0) {
//这个地方很难理解,而且有bug,已经被人发现后提交,oracle也在后续的jdk版本中修复了,我们每个条件挨着看一下
//(sc >>> RESIZE_STAMP_SHIFT) != rs
// 这个很好理解,rs在首次有线程扩容时(见下面的elseif条件),已经将低16位移到高16位(左移)并赋值给sizeCtl,
// 这里将sc(即为sizeCtl) 无符号右移16位,再跟rs比较,用来判断是否还是当前的扩容标识戳,
// 如果不一致,说明table已经被扩容完毕,这里是再一次的扩容
// 满足这个条件则break,跳出循环
//sc == rs + 1 || sc == rs + MAX_RESIZERS
// 这两个条件结合起来看,是用来判断扩容线程数的,但这个地方是个Bug,应该是sc == (rs << RESIZE_STAMP_SHIFT) + 1 || sc == (rs << RESIZE_STAMP_SHIFT) + MAX_RESIZERS
// 已经被修复,可以看这个https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8214427 ,我们按照正确的代码解释
// 首次扩容时sc的值为(rs << RESIZE_STAMP_SHIFT) + 2,后续每增加一个扩容线程,则会对sc + 1,
// 所以sc == (rs << RESIZE_STAMP_SHIFT) + 1这一句表示所有的线程都扩容完退出了,扩容工作已经结束。
// sc == (rs << RESIZE_STAMP_SHIFT) + MAX_RESIZERS 这一句标识当前扩容线程数已经达到了上限,当前线程无法再参与扩容了
// 这两个条件满足其一,则break,跳出循环
//(nt = nextTable) == null
// 这个条件,nextTable是扩容的时候用的,为空则表示扩容工作已经结束。
// 满足这个条件,则break,跳出循环
//transferIndex <= 0 transferIndex的含义是,当前最小的已被分配给线程的数组元素下标,因为数组是从高到底迁移的,因此这个变量可以代表多线程迁移数组的进度。
// 小于等于0表示,当前数组的所有元素都已经分配给线程处理,当前线程不需要再帮助扩容了。
/* 关于刚才提到的bug,jdk修复后的源码为
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n) << RESIZE_STAMP_SHIFT;
if (sc < 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);
}
else if (U.compareAndSetInt(this, SIZECTL, sc, rs + 2))
transfer(tab, null);
s = sumCount();
}
*/
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
// 不满足上面的if条件,则表示需要当前线程参与扩容
// 首先使用cas的方式将sizeCtl 加1 ,成功后调用扩容方法
// 此处体现sizeCtl的一个含义,即在扩容时,sizeCtl的高16位表示扩容的标识戳,低16位表示当前正在扩容的线程数+1。
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
//第一次有线程进行扩容,先把sizeCtl的值使用cas的方式更新为(rs << RESIZE_STAMP_SHIFT) + 2),然后执行扩容方法
//对(rs << RESIZE_STAMP_SHIFT) + 2)的解释:
//经过resizeStamp计算后的rs,可以看到高16位都是0,低16位是有效的标识戳。
//以下面resizeStamp方法的例子,经过此次运算后的值为
// rs = 0000 0000 0000 0000 1000 0000 0001 1011
// rs << RESIZE_STAMP_SHIFT = 1000 0000 0001 1011 0000 0000 0000 0000
// (rs << RESIZE_STAMP_SHIFT) + 2) = 1000 0000 0001 1011 0000 0000 0000 0010
// 为什么要+2,我先往下看看
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
//调用扩容方法
transfer(tab, null);
//扩容完重新计算size,再走一次while判断,看是否还需要进一步扩容
s = sumCount();
}
}
}
fullAddCount方法
private final void fullAddCount(long x, boolean wasUncontended) {
int h;
//获取当前线程探针值赋值给h,并检查是否为0,为0则表示当前线程尚未用到过ThreadLocalRandom.getProbe(),需要先进行初始化。
if ((h = ThreadLocalRandom.getProbe()) == 0) {
//ThreadLocalRandom进行初始化
ThreadLocalRandom.localInit(); // force initialization
//取得ThreadLocalRandom.getProbe(),重新赋值给h
h = ThreadLocalRandom.getProbe();
//这个标记的是外面传进来的,false的意思是曾经尝试cas的方式更新cellValue,但是失败了
//此处探针刚刚初始化完,所以肯定不会是曾经更新失败过,因此置为true
wasUncontended = true;
}
boolean collide = false; // True if last slot nonempty
for (;;) {
CounterCell[] as; CounterCell a; int n; long v;
// 1、counterCells 不为空且length>0,同时完成as和n的赋值
if ((as = counterCells) != null && (n = as.length) > 0) {
//1.1、探针位置的元素为空 h&(length - 1)
if ((a = as[(n - 1) & h]) == null) {
//cellsBusy == 0,当前没有其他线程在操作数据组(包括扩容或创建数组元素)
if (cellsBusy == 0) { // Try to attach new Cell
//声明一个CounterCell r,并通过x进行初始化
CounterCell r = new CounterCell(x); // Optimistic create
//再次检查cellsBusy是否为0,并通过cas的方式将cellBusy赋值为1,保证其他线程进不来
if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
//开始创建
boolean created = false;
try { // Recheck under lock
CounterCell[] rs; int m, j;
//再次检查counterCells不为空并且当前探针指向的元素为Null
if ((rs = counterCells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
//直接把新创建的CounterCell复制到探针对应下标的元素上,置created为true,完成计数。
rs[j] = r;
created = true;
}
} finally {
//cellsBusy释放
cellsBusy = 0;
}
//如果创建成功,则跳出循环
if (created)
break;
//否则直接进行下一次循环
continue; // Slot is now non-empty
}
}
collide = false;
}
//1.2、走到这说明当前探针指向的元素不为空,先判断一下如果传入的wasUncontended为false,则说明是在给探针元素的cellValue赋值是竞争失败导致的。
else if (!wasUncontended) // CAS already known to fail
//赋值为true,走到下面去rehash,rehash的目的是改变探针的值,换一个数组元素,然后继续循环
wasUncontended = true; // Continue after rehash
//1.3、继续竞争一下当前探针指向的元素的cellvalue,成功了跳出。
//注意:只有wasUncontended为true的时候才会走这一步,如果为false,上面的判断就会拦住后去rehash。
// wasUncountended为true有这么几种情况:
// 1、进入该方法的时候,走了探针初始化,该探针尚未使用,因此可以尝试一下是否能更新成功。
// 2、上面的else if判断时为false,进入到分支后,置为了true,并且经过了下面rehash的方式更新了探针的值,因此此时也可以尝试下能否更新成功。
else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
//如果返回true,说明更新成功,则直接跳出循环。否则继续循环。
break;
//1.4、这一步要结合collide变量来看,collide==true意味着数组需要扩容了,但是是否执行扩容有两个限制条件,满足其一则不扩容。
// 1)、counterCells != as 说明当前有其他线程正在扩容,则collide=false,走rehash后重新更新(即上面的1.1和1.3)的路子。
// 2)、n >= NCPU 数组上线已经等于或超过当前服务器的核心数,则collide=false,走rehash后重新更新(即上面的1.1和1.3)的路子。达到这个条件意味着数组永远不会再被扩容了,只能在当前数组内不断的rehash,更改探针位置,知道成功。
else if (counterCells != as || n >= NCPU)
collide = false; // At max size or stale
//1.5、走到这一步说明上面的几个判断条件都没拦住
// 意味着:当前探针指向的数组位置不为null,且经过一次cas更新cellvalue失败了,且没有达到上面的限制条件。
// 第一次走到这还是先不要扩容,先把collide改为true,再走一次rehash试试。下次到这里的时候如果还是true,说明已经给过一次机会,但还是失败了。
// 这样就不会走到这个else if,而是走进下一个判断,直接扩容。
else if (!collide)
collide = true;
//1.6、走到这里说明需要库容了,先判断一次cellsBusy==0,并且使用cas的方式把cellsBusy改为1.
// cellsBusy==0 的判断,是看一下当前有没有其他线程正在操作cellsBusy,有的话,就先不扩容。
// U.compareAndSwapInt(this, CELLSBUSY, 0, 1) 把cellsBusy成功改成1,确保其他线程不会在扩容期间操作数组。
else if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
try {
//进来后还是先判断一下,as是否等于counterCells,如果不相等,说明有其他线程在进行扩容或已经扩容完毕,则退出。
if (counterCells == as) {// Expand table unless stale
//生命一个counterCell数组,长度是之前的两倍
CounterCell[] rs = new CounterCell[n << 1];
//将原数组的值复制到新数组的低n位上
for (int i = 0; i < n; ++i)
rs[i] = as[i];
//将新数组付给counterCells
counterCells = rs;
}
} finally {
//无论是否扩容,都释放这个标记
cellsBusy = 0;
}
//刚刚扩容完(要么是自己扩容的,要么是别的线程扩容的),肯定不需要再次扩容,因此collide = false
collide = false;
//continue 使用当前的探针值和新的数组,重新尝试一下(上面的逻辑,重新计算探针对应的数组下标位置,重新尝试更新)
continue; // Retry with expanded table
}
//重新计算探针值,再次循环尝试,看能否成功。
h = ThreadLocalRandom.advanceProbe(h);
}
//2、走到这counterCells为空,再次校验一下counterCells == as,且当前没有线程在操作数组(cellsBusy==0)
// 使用cas的方式将cellsBusy更改为1,防止其他线程在初始化期间操作数组
else if (cellsBusy == 0 && counterCells == as &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
//生命一个标记,标记一下是否初始化成功
boolean init = false;
try { // Initialize table
//再次判断counterCells == as,如果不相等,说明有其他线程已经初始化完毕
if (counterCells == as) {
//首次创建数组,先创建两个元素
CounterCell[] rs = new CounterCell[2];
//h & 1,使用探针值&1,返回值只有0或1,随机制定数组下标为0或1的元素,使用x进行初始化
rs[h & 1] = new CounterCell(x);
//将初始化完的数组赋值给counterCells
counterCells = rs;
//标记初始化成功
init = true;
}
} finally {
//无论如何,释放cellsBusy
cellsBusy = 0;
}
if (init)
//如果初始化成功了,说明x的增加也完成了,直接退出。
break;
}
//3、如果当前数组为空(不满足1条件),且自己也没有竞争到机会去初始化(不满足2条件),那就再竞争一次baseCount,成功了就退出。
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
break; // Fall back on using base
}
}