ConcurrentHashMap源码(JDK1.8)深度解析(没有比这更详细的了)-计数

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
        }
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值