ConcurrentHashMap(细节篇)

天涯浪子 提交于 2021-02-02 01:38:36

本篇文章接上篇文章,介绍一些ConcurrentHashMap的细节,虽然不是特别重要,但也是干货满满。
先列一个参数:

 //0:表示counterCells无人使用
 //因为counterCells是一个数组,线程不安全的
 //所以在操作counterCells之前,都要用CAS将cellsBusy改成1,表示有人在使用该数组
 private transient volatile int cellsBusy;

spread():

spread(int h):

		//跟hashmap有点区别,不仅高16参与了计算,并且还再次 & 计算
        return (h ^ (h >>> 16)) & HASH_BITS;

前面运算这里不解释,跟hashMap一样,主要看后面的& HASH_BITS

        HASH_BITS是0x7fffffff,换算下来是2147483649int的最大值
        作用是消除负数,这里顺便看下负数的二进制
        举个例子:
                -5的原码是:10000000000000000000000000000101
                    反码是:11111111111111111111111111111010
                    补码是:11111111111111111111111111111011

负数的原码:是对应的正数的二进制,但是符号位是1。
符号位是区分正负数用的,1是负数,0是整数(符号位就是最高位的意思,左边第一位)
负数的反码:是除符号位,其余的取反
负数的补码:是反码+1
HASH_BITS换算下来的二进制是:11111111111111111111111111111111(注意是31位,虽然Java中的int是32位,但是一般只有负数才会显示32位,正数默认31位)
hash & HASH_BITS,不管hash是正还是负,结果都会转成正数,因为HASH_BITS的符号位是0,& 下来的最高位肯定是0,也就是正数。
spread(int h) 的作用其实就是避免hash值是负数,大概是因为ConcurrentHashMap内置了MOVEDTREEBINRESERVED这3个hash,是负数,为了避免冲突吧。





addCount的第一部分:

        //判断是否有失败的风险
        if ((as = counterCells) != null ||
                !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
   
   
            CounterCell a; long v; int m;
            boolean uncontended = true;
            //判断是否可以投机
            if (as == null || (m = as.length - 1) < 0 ||
                    (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
                    !(uncontended =
                            U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
   
   
                  //死循环把x记录到counterCells,uncontended是判断有没有尝试用CAS更新过
                fullAddCount(x, uncontended);
                return;
            }
              //到这表示CAS把值记到counterCells肯定成功了,因为上面的是 ||,CAS是最后一个判断条件
              //check == 0:表示插入的下标是空的,此时是头节点,此坑位还有不少直接return
              //check == 1:表示插入的是链表的第2个,此坑位还有不少,就不考虑扩容,直接return
              //check == -1:表示的是删除,直接return
              //这里跟hashMap有点不一样,没有直接判断大于多少就扩容
            if (check <= 1)
                return;
              //把counterCells的值累加到baseCount
            s = sumCount();
        }

先介绍一下counterCells,每次是CAS去增加baseCount的,这样并发的话肯定会有线程更新失败,这时候ConcurrentHashMap并没有重试,而是把失败的值记录下来,就是记录到counterCells里面。
这里可以分为2步:

  1. 判断是否有失败的风险
  2. 判断是否可以投机(开玩笑)

判断是否有失败的风险

if ((as = counterCells) != null ||
      !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
   
   

(as = counterCells) != null:

很简单,counterCells初始为null,CAS增加baseCount失败就会把值记录到counterCells里
counterCells != null说明出现并发修改baseCount,有线程失败了
已经有线程失败了,那么再CAS新增很有可能失败,所有这里 != null 就没有尝试CAS,直接走下面的逻辑

为null才走CAS,尽可能的避免竞争

判断是否可以投机

//进到这里,表示很有可能多线程在修改baseCount
//ThreadLocalRandom.getProbe()可以简单的先理解成取当前线程的hash,后面再详细分析
//非常Nice的判断
if (as == null || (m = as.length - 1) < 0 ||
      (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
      !(uncontended =
              U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
   
   
    //死循环把x记录到counterCells,uncontended是判断有没有尝试用CAS更新过
  fullAddCount(x, uncontended);
  return;
}

as == null || (m = as.length - 1) < 0:

这个判断很简单,判断counterCells是否有值,但是含义却不简单
因为上面已经判断过 != null了,这里再判断一下,为了减少竞争,继续看下面的判断

(a = as[ThreadLocalRandom.getProbe() & m]) == null:

ThreadLocalRandom.getProbe()最后说,先简单理解成线程的hash
这里判断的是根据当前线程推算出要插入的counterCells的下标是否为null
走到这里,counterCells!= null,再判断插入的下标是否= null

U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x):

因为if里面是 || 判断,走到这,说明counterCells != null,要插入的counterCells的下标 =null
实在没办法了,必须得竞争了,此时才CAS,把值更新到counterCells 里面

回忆一下,整个判断,是这样的:

  1. counterCells == null,CAS更新baseCount
  2. counterCells != null(或者CAS失败),再判断一遍counterCells != null
  3. counterCells != null,再判断要插入的下标!= null
  4. 要插入的下标!= null,才CAS把值更新到counterCells

可以看到,尽量的保证进入到fullAddCount这个死循环的时候,没有竞争。
实在避免不了,再进死循环之前,再尝试一遍CAS,非常nice,Doug Lea牛逼。

fullAddCount():

继续fullAddCount
由于较为复杂,分成3部分看:

  1. counterCells != null,找到的下标 == null
  2. 找到的下标 != null
  3. counterCells == null

counterCells != null,找到的下标 == n

private final void fullAddCount(long x, boolean wasUncontended) {
   
   
        int h;
          //判断当前线程的PROBE是否初始化过
        if ((h = ThreadLocalRandom.getProbe()) == 0) {
   
   
              //为0就先初始化
            ThreadLocalRandom.localInit();
              //获取值
            h = ThreadLocalRandom.getProbe();
              //把标志位置为true,确保为true,也就是确保就是没有CAS过
            wasUncontended = true;
        }
        boolean collide = false;                // True if last slot nonempty
        for (;;) {
   
   
            CounterCell[] as; CounterCell a; int n; long v;
              //先判断counterCells是否初始化过
            if ((as = counterCells) != null && (n = as.length) > 0) {
   
   
                  //h是线程的随机值,(n - 1) & h就是计算下标
                if ((a = as[(n - 1) & h]) == null) {
   
   
                      //cellsBusy为0,表示没线程在初始化或者扩容counterCells
                    if (cellsBusy == 0) {
   
              
                          //生成要插入的对象
                        CounterCell r = new CounterCell(x); 
                          //把cellsBusy改成1,表示有线程再用
                        if (cellsBusy == 0 &&
                                U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
   
   
                            boolean created = false;
                            try {
   
    
                                CounterCell[] rs; int m, j;
                                  //把cellsBusy改成1,再判断一下
                                  //因为改成1之后,counterCells就锁定了,此时的判断就是准的
                                if ((rs = counterCells) != null &&
                                        (m = rs.length) > 0 &&
                                        rs[j = (m - 1) & h] == null) {
   
   
                                      //赋值
                                    rs[j] = r;
                                    created = true;
                                }
                            } finally {
   
   
                                  //最终将cellsBusy置为0
                                cellsBusy = 0;
                            }
                            if (created)
                                break;
                            continue;
                        }
                    }
                    collide = false;
                }

总的来说,还是比较简单的,就是一个普通数组,用cellsBusy来保证线程安全。
不过有一个判断比较巧妙,需要单独说一下:

if ((h = ThreadLocalRandom.getProbe()) == 0) {
   
   
		//记住这个变量
		wasUncontended = true;

这里不仅仅是判断PROBE是否初始化过,还有一个意义wasUncontended

其实这里判断的是进方法之前,有没有CAS过
== 0说明没有初始化,说明进fullAddCount是因为counterCells为null
!= 0说明初始化过了,说明进fullAddCount之前已经走过getProbe()了,也就是很有可能CAS过

wasUncontended == true:说明没有CAS
wasUncontended == false:说明CAS失败了

找到的下标 != null

                //走到这,说明计算的下标不为null
                else if (!wasUncontended)
                    //进到里面,说明之前在addCount已经CAS过了,并且失败了
                    //第一次只把标志位改为true,改个状态出去,给个机会,第二次再来CAS,避免竞争
                    wasUncontended = true;
                //走到这,2种可能
                //1:是死循环的第二次了
                //2:是之前没有CAS过
                //不管哪种情况,必须要CAS了,要么是第一次,要么已经让过一次了,只能竞争了。
                else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
                	//成功就退出了
                    break;
                //判断一下counterCells是否已经被改掉了或者说数组长度已经 >= CPU数量了
                //counterCells 最大是NCPU
                else if (counterCells != as || n >= NCPU)
                    collide = false;
                else if (!collide)
                	//进到里面,说明需要扩容了
                	//改个状态出去,给个机会
                    collide = true;
                //走到这,没办法了,准备扩容吧
                //尝试CAS标志cellsBusy
                else if (cellsBusy == 0 &&
                        U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
   
   
                    try {
   
   
                        //双重校验,一个意思
                        if (counterCells == as) {
   
   
                            //扩容2倍
                            CounterCell[] rs = new CounterCell[n << 1];
                            //把旧值复制到新数组
                            for (int i = 0; i < n; ++i)
                                rs[i] = as[i];
                            counterCells = rs;
                        }
                    } finally {
   
   
                        //最终将cellsBusy置为0
                        cellsBusy = 0;
                    }
                    //改为false,下次还要扩容又要2次判断
                    collide = false;
                    //跳出,继续尝试把值新增到数组
                    continue;
                }
                //每次不成功就算下新的hash,换个下标试试
                h = ThreadLocalRandom.advanceProbe(h);
            }
           
        }
    }

同样不是很难,只是有一点确实值得借鉴,concurrentHashMap真的是极力避免竞争,上面说的给个机会,其实相当于空轮询,竞争失败之后,基本上都要空轮询一次,第2次才会继续竞争。

counterCells == null

			//走到这,说明counterCells为null
            //就尝试把CELLSBUSY改完1
            else if (cellsBusy == 0 && counterCells == as &&
                    U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
   
   
                boolean init = false;
                try {
   
                              // Initialize table
                    //双重校验,一个意思
                    if (counterCells == as) {
   
   
                        //初始化长度为2
                        CounterCell[] rs = new CounterCell[2];
                        //h是线程的随机值,h & 1就是计算下标
                        rs[h & 1] = new CounterCell(x);
                        counterCells = rs;
                        //设置初始化状态init为true
                        init = true;
                    }
                } finally {
   
   
                    //最终将cellsBusy置为0
                    cellsBusy = 0;
                }
                if (init)
                    //初始化好了,且新增好了就跳出
                    break;
            }
            //走到这,说明已经有线程在初始化counterCells,就再尝试一下baseCount + 1,不行就死循环
            else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
                break;

这也不难,就是普通 数组,线程安全的初始化。
拆开来,貌似都不难,但是值得借鉴的确实多。

ThreadLocalRandom.getProbe():
UNSAFE.getInt(Thread.currentThread(), PROBE)

很简单,就是获取当前线程的某个变量值
看下PROBE

    private static final sun.misc.Unsafe UNSAFE;
    private static final long PROBE;
    static {
   
   
        try {
   
   
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> tk = Thread.class;
                (tk.getDeclaredField("threadLocalRandomProbe"));
        } catch (Exception e) {
   
   
            throw new Error(e);
        }
    }

是获取的threadLocalRandomProbe的初始地址的偏移量,简单理解成获取值。
看下threadLocalRandomProbe:

    @sun.misc.Contended("tlr")
    int threadLocalRandomProbe;

就是一个变量,那可以肯定,第一次ThreadLocalRandom.getProbe()肯定为0

ThreadLocalRandom.localInit():
	private static final int PROBE_INCREMENT = 0x9e3779b9;
	private static final AtomicInteger probeGenerator = new AtomicInteger();
    static final void localInit() {
   
   
    	//很简单,获取一个值0x9e3779b9,-1640531527
        int p = probeGenerator.addAndGet(PROBE_INCREMENT);
        int probe = (p == 0) ? 1 : p;
        //赋值
        UNSAFE.putInt(t, PROBE, probe);
    }

说明一下:
threadLocalRandomProbe是Thread的变量
probeGenerator 、PROBE_INCREMENTThreadLocalRandom的,每调一次localInitprobeGenerator的值也在变。
看完整个PROBE的过程,理解成线程的hash是没啥问题的。


ThreadLocalRandom.advanceProbe():
	static final int advanceProbe(int probe) {
   
   
        probe ^= probe << 13;   // xorshift
        probe ^= probe >>> 17;
        probe ^= probe << 5;
        UNSAFE.putInt(Thread.currentThread(), PROBE, probe);
        return probe;
    }

一通计算,PROBE肯定变了,每次换个值,也就是换个下标。

面试题:concurrentHashMap的key和value为啥不能为null

先来回忆一下hashMap:

	static final int hash(Object key) {
   
   
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

concurrentHashMap:

	final V putVal(K key, V value, boolean onlyIfAbsent) {
   
   
        if (key == null || value == null) throw new NullPointerException();

可以看到,hashMap是允许的,concurrentHashMap不允许。

先说value不允许为null:
concurrentHashMap定位的是并发场景,如果允许valuenull,那么就会造成一种情况。

A线程:get(key)null,这是A是不知道keyvaluenull,还是key不存在
如果是hashMap,可以用contains来判断,而concurrentHashMap不行。
B线程:在Aget(key) 之后put(key,null)
此时Acontains就是true,那么就以为key当时是有值的,不知道是B中途插入的。
hashMap定位的是单线程,containstrue,就表示为key当时是有值的,不考虑B中途插入的情况。



有人会说,别的value不为null,也会发生这种情况。
没错,但是get(key)nullcontainstrue,A能知道有人中途插入了。
先说key不允许为null:
value不允许为null:同样的原因,containsKey的时候有二义性


标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!