ConcurrentHashMap源码分析

13 篇文章 2 订阅

1 创建

1.1 构造器

创建HashMap的时候,可以指定初始化容量,也可以不指定初始化容量

//不指定初始化容量
public ConcurrentHashMap() {}
//指定初始化容量
public ConcurrentHashMap(int initialCapacity) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException();
    //如果指定的容量大于最大容量,则使用最大容量,否则对通过tableSizeFor对指定容量进行处理后的值
    int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
               MAXIMUM_CAPACITY :
               tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
    this.sizeCtl = cap;
}
1.2 tableSizeFor

tableSizeFor方法是返回大于等于C的最小2的幂。所以,ConcurrentHashMap要做的事情就是要保证其容量必须是2的幂。为什么要这样做后面会详细说明。

private static final int tableSizeFor(int c) {
    int n = c - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

需要注意一个点,再ConcurrentHashMap通过tableSizeFor处理我们给定的容量时,传入的参数是initialCapacity + (initialCapacity >>> 1) + 1
这个是ConcurrentHashMap为开发人员的考虑,和扩容有关,这里先简单说明一下:当ConcurrentHashMap的容量达到阀值容量的时候,就会扩容。而扩容阀值计算为:

  • 阀值 = n - (n >>> 2) = n * 0.75

站在使用ConcurrentHashMap的角度,只需要关心自己的数据需要的空间,至于自己的数据量是否会引起出现扩容,不用考虑,ConcurrentHashMap通过initialCapacity + (initialCapacity >>> 1) + 1已经帮我们申请到了足够的空间。

ConcurrentHashMap是数组加链表/红黑树的方式存储数据,而在创建ConcurrentHashMap的时候,不管是否指定了容量,都不会初始化数组,其实初始化的操作,是在第一次put操作的时候做的,这样避免了初始化了容量并不存储数据而占用空间的情况。

2 初始化

tab数组是在第一次put操作的时候进行初始化的:

final V putVal(K key, V value, boolean onlyIfAbsent) {
     //......
     for (Node<K,V>[] tab = table;;) {
         Node<K,V> f; int n, i, fh;
         //如果tab为null,则进行初始化
         if (tab == null || (n = tab.length) == 0)
             tab = initTable();
         else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
             //......
         }
        //......
     }
     //...
}
2.1 initTable初始化
private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    //tab没有初始化成功的时候就无限循环,直到初始化好为止跳出循环
    while ((tab = table) == null || tab.length == 0) {  
    	//这里把sizeCtl的值赋值给了局部变量sc。如果创建ConcurrentHashMap给定了初始容量值,此时sizeCtl是有值的。
    	//这里只是暂时将sizeCtl初始值先放到sc中
    	//这里默认sizeCtl值为0,为什么会小于0,在else逻辑中能看到
    	//通过else的逻辑可知:如果sc小于0,那么说明其他线程已经在初始化了,所以这里就通过yield + 循环方式等待初始化,只等循环条件不满足(初始化好)跳出
        if ((sc = sizeCtl) < 0)
            Thread.yield(); // lost initialization race; just spin
        //因为可能多个线程同时访问并初始化,所以这里需要通过CAS做拦截,只允许一个线程初始化成功。
        //如果一个线程已经将SIZECTL值修改为-1那么其他的线程就不能就行初始化了,就会在if中通过yield + 循环方式等待初始化
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                if ((tab = table) == null || tab.length == 0) {
                	//sc > 0说明创建ConcurrentHashMap的时候指定了初始化容量,如果没有指定则使用默认容量16
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    //这里就是创建一个tab数组
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    //这里计算出sc,并在finally 中赋值给sizeCtl。
                    //这里计算过程等同于 sc = n * 0.75。 n >>> 2 = n / 4。n >>> 1 = n / 2
                    //这里计算的额sc,在扩容的时候,容量达到sc的时候就会做扩容处理
                    sc = n - (n >>> 2);
                }
            } finally {
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

初始化要点:

  • 初始化在第一次put的时候完成。
  • 可能有多个线程同时初始化,通过U.compareAndSwapInt(this, SIZECTL, sc, -1)控制只能一个线程初始化成功。其他线程如果发现已经有线程正在初始化,则通过while + yiled方式等待初始化成功。
  • 初始化成功后,设置sizeCtl = n - (n >>> 2) ,相当于sizeCtl = n * 0.75 ,通过移位运算,速度会更快。这里就是设置扩容阀值,当存储容量超过sizeCtl = n - (n >>> 2),就会进行扩容。

3 put

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    //获取key的hash值
    int hash = spread(key.hashCode());
    //binCount用来记录当前的链表的长度
    int binCount = 0;
    //无限循环
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        //第一次设置值得时候,tab还没有被初始化,tab的值就为null,就需要初始化
        //第一次访问初始化结束后,会进行下一次循环将值放入tab
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        //这里就是从tab中,通过前面tab计算出的hash取出对应的tab[i],
        //这里为什么用tabAt方法而不直接用数组下标取,可以看tabAt方法注解。
        //如果取出的位置的Node为null,则通过CAS的方式放值,不为null,说明出现了hash冲突,则走else流程。
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
        	//因为同时可能又多个线程同时放。只允许一个成功,失败的通过下一次循环。
        	//如果放值成功了,则break跳出循环
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        //这个地方,如果== MOVED,说明正在扩容,则调用helpTransfer,即,帮助扩容。
         //(3)*********************
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        //执行到这里,说明出现了Hash冲突,就需要将数据放入链表或者红黑树中
        else {
            V oldVal = null;
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                	//这里fh >= 0,说明当前tab的这个位置还是数组。
                	//红黑树的节点hash值会设置成-2
                    if (fh >= 0) {
                        binCount = 1;
                        //遍历链表,放置Node
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            //如果hash和key都是相等的,就覆盖value
                            //这里是否覆盖,通过onlyIfAbsent判断,这个onlyIfAbsent可以再put的时候指定
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            //将新增的Node放到链表末尾
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    //判断是否是红黑树节点,如果是,则将数据放入红黑树
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            //如果链表的长度大于等于8,则需要将其转为红黑树
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    //执行到这儿,说明值已经放置成功了,需要对count进行++的操作
    //当有很多的线程同时对同一个count进行++操作,肯定会出现线程安全问题。addCount需要使用CAS保证线程安全。
    //但是大量的线程同时addcount,只有一个线程成功,其他的线程都在自旋,很浪费资源,所以addcount采用了分而治之的思量进行计数。
    addCount(1L, binCount);
    return null;
}

put主要步骤
(1)如果put的时候,发现tab还是null,说明tab还没有被初始化,需要进行初始化。

(2)通过hash得到对应的tab下标的时候,使用的是(n - 1) & hash),我们平时如果要散列,使用的都是 hash % n, 其实在这里这两种方式是等价的,因为tab的长度是2的幂,所以才是等价,这也是tab的长度要保持在2的幂的一个原因。

(3)在获取tab数组对应位置的Node的时候,使用的是f = tabAt(tab, i = (n - 1) & hash),而非tab[ i = (n - 1) & hash]。tabAt实现如下:

 transient volatile Node<K,V>[] table;
//tabAt中没有通过数组下标直接取值,而是通过getObjectVolatile去取。
//原因:全局变量table虽然被修饰为了volatile,但是其只是一个指针,其指针地址发成变化,才会被其他线程立即可见
//因为他是一个数组,其内部的值并没有被volatile修饰,所以其内部的值发生了变化,通过普通的取值方式不能被立即可见,所以要通过getObjectVolatile去取值,才会立即可见
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
    return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}

tab数组引用虽然被volatile修饰,但是tab里面的值并没有被volatile修饰,所以为了保证多线程之间数组内容改变后的立即可见,所以就使用了Unsafe类中的getObjectVolatile,在执行的时候加入内存屏障,保证了修改后线程间的立即可见。

(4)通过判断如果 (fh = f.hash) == MOVED,则执行helpTransfer,在这里如果条件满足,说明当前曾在扩容,后面你会看到,在扩容的时候,如果当前tab对应的链表或者红黑树已经转移完成,则会将tab对应的位置设置为ForwardingNode,而ForwardingNode的hash值就是MOVE。

(5)真正的进行put数据了,此时会判断当前tab的位置是链表还是红黑树,如果是链表,则将Node添加到链表的末尾,如果是红黑树,就通过红黑树的逻辑加入节点即可。这里区别链表和红黑是使用的判断是fh >= 0则为链表,之所以这样判断,是因为链表在tab中存储的节点是TreeBin,在将TreeNode转换为TreeBin的时候,将其hash设置为 -2:,代码如下:

static final int TREEBIN   = -2; // hash for roots of trees

TreeBin(TreeNode<K,V> b) {
	//创建根节点的时候,这里把hash设置为TREEBIN == -2,
	//这也就是前面判断,如果hash > 0,则为链表,否则为红黑树的原因
    super(TREEBIN, null, null, null); 
    //......
    //关于红黑树的逻辑,这里不分析
}

4 AddCount

每次执行完put,在put的末尾,都会执行addCount计数:

final V putVal(K key, V value, boolean onlyIfAbsent) {
   //......
    //执行到这儿,说明值已经放置成功了,需要对count进行++的操作
    //当有很多的线程同时对同一个count进行++操作,肯定会出现线程安全问题。addCount需要使用CAS保证线程安全。
    //但是大量的线程同时addcount,只有一个线程成功,其他的线程都在自旋,很浪费资源,所以addcount采用了分而治之的思量进行计数。
    addCount(1L, binCount);
    return null;
}

计数逻辑整体思路:
如果我们只使用一个Long类型进行扩容。那势必在扩容的时候我们需要对计数器的++操作需要加锁。如果我们使用CAS避免锁操作,但是在多线程的情况下,也只有一个线程能CAS成功,其他线程会浪费CPU的资源。所以为了提高计数效率,ConcurrentHashMap就采用数组进行计数,将多个线程的++操作散列到数组的不同位置,这样可以有效的缓解线程的竞争,提升技术效率。

当然,如果我们虽然使用了ConcurrentHashMap,但是不存在多线程竞争计数的情况,次数就没必要使用数组技术,可以直接使用一个Long类型即可。

所以ConcurrentHashMap的计数就分为两部分,一个是baseCount,在不存在线程竞争的情况下使用。如果有多线程竞争计数,则会创建一个CounterCells数组来计数。最终中的数据 = baseCount + 数组数量总和。

addCount实现如下:

private final void addCount(long x, int check) {
    CounterCell[] as; long b, s;
    //首先判断counterCells是否为null,如果是,则将值累加到baseCount。
    //如果只有不存在竞争,累加baseCount肯定是成功的。
    //一旦counterCells不为null,或者对baseCount使用CAS失败,则就要使用counterCells进行计数了
    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))) {
            //fullAddCount包含了CounterCell初始化,扩容,计数累加的功能
            fullAddCount(x, uncontended);
            return;
        }
        if (check <= 1)
            return;
        s = sumCount();
    }
    //下面是扩容的逻辑,可以先忽略
    if (check >= 0) {
    	//这里开始扩容的逻辑
    	//扩容的时候支持多个线程同时扩容,会使用SIZECTL记录当前参与扩容的线程的数量。
    	//使用SIZECTL的高16位作为扩容戳,扩容戳计算和当前tab长度有关,详细的看resizeStamp
    	//使用SIZECTL的第16位记录当前参与扩容的线程的数量。参与扩容的线程的数量 = 低16位 - 1
        Node<K,V>[] tab, nt; int n, sc;
        //在while里面为sc赋值了,sc = sizeCtl = 扩容阈值 = count * 0.75
        //只有当已经存放的数量 > 扩容阀值,才进行扩容操作
        //这里还有一点需要注意:因为支持多个线程同时进行扩容,而一开始扩容,sizeCtl的值一直将会是负数。
        //只有当扩容结束,sizeCtl才会被重新设置为扩容阀值。所以,想要退出循环,必须等到扩容结束。
        while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
               (n = tab.length) < MAXIMUM_CAPACITY) {
            //这里拿到的rs,第16位一定为1
            int rs = resizeStamp(n);
            //第一次,sc一定不小于0,执行else
            //第一次执行else,在else中,会将其设置为负数。
            if (sc < 0) {
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                    transferIndex <= 0)
                    break;
                //继续有其他线程来扩容,为SIZECTL加1记录线程数量。
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                    transfer(tab, nt);
            }
            //因为会有多个线程同时进入到这里进行扩容,因为公用SIZECTL来记录当前有多少个线程参与扩容,所以要使用CAS修改SIZECTL
            //第一次进行的时候,设置SIZECTL = rs << RESIZE_STAMP_SHIFT) + 2。得到的一定是负数,需要看resizeStamp
            else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                         (rs << RESIZE_STAMP_SHIFT) + 2))
                //transfer是真正的扩容逻辑
                transfer(tab, null);
            s = sumCount();
        }
    }
}

(1)首先就判断了counterCells不等于空。则肯定存在过线程竞争,则直接使用counterCells进行计数。如果counterCells等于空,则尝试使用CAS + baseCount计数,如果CAS失败了,说明发生了竞争,则使用counterCells计数。

(2)要使用counterCells计数,那counterCells位null的情况下,必须要执行fullAddCount初。如果counterCells不为空,则通过ThreadLocalRandom.getProbe() & as.(length - 1)随机数 + 取模的方式(后面会看到,这里之所以不用%,也是因为counterCells数组长度是2的幂)拿到计数数组对应位置的counterCell进行计数。如果对应位置的counterCell为null,则执行fullAddCount。如果对应位置的counterCell不为null,则通过CAS进行计数,如果CAS失败,说明存在竞争,则执行fullAddCount。

所以,fullAddCount包含了counterCells数组和各个位置counterCell的初始化,以及线程竞争情况下的CAS计数,

在fullAddCount中频繁用到cellsBusy变量,这里提前介绍一下,方便理解后面的代码:

private transient volatile int cellsBusy;

因为fullAddCount具有初始化,扩容,计数的功能。在多线程的情况下,会通过cellsBusy变量加上CAS进行控制:
(1)保证只能一个线程初始化成功。
(2)保证在扩容的时候不允许进行CountCells对应数组位置的CountCell初始化操作。

fullAddCount实现如下:

private final void fullAddCount(long x, boolean wasUncontended) {
        int h;
        //从ThreadLocalRandom中获取随机数
        if ((h = ThreadLocalRandom.getProbe()) == 0) {
        	//如果发现获得的probe值为0,说明ThreadLocalRandom还没有被初始化,需要先进行初始化。
            ThreadLocalRandom.localInit();      // force initialization
            //初始化后,再重新获取一个随机数
            h = ThreadLocalRandom.getProbe();
            wasUncontended = true;
        }
        boolean collide = false;                // True if last slot nonempty
        for (;;) {
            CounterCell[] as; CounterCell a; int n; long v;
            //如果counterCells为null,说明counterCells还没初始化,所以需要对counterCells进行初始化
            if ((as = counterCells) != null && (n = as.length) > 0) {
                //如果counterCells不为null,就获取counterCells相应位置的counterCell
                //如果定位到的counterCell为null,说明counterCells相应位置的counterCell还是null,需要new一个counterCell
                if ((a = as[(n - 1) & h]) == null) {
                	//执行到这儿,说明定位到的位置CounterCell为null,需要给相应位置new一个CounterCell,并赋值。
                	//只有cellsBusy为0的情况下,才允许进行CountCells对应数组位置的CountCell初始化操作,因为此时可能正在扩容
                    if (cellsBusy == 0) {            // Try to attach new Cell
                    	//创建一个CounterCell
                        CounterCell r = new CounterCell(x); // Optimistic create
                        //进行CountCells对应数组位置的CountCell初始化操作,也不允许扩容操作
                        if (cellsBusy == 0 &&
                            U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                            boolean created = false;
                            try {               // Recheck under lock
                                CounterCell[] rs; int m, j;
                                //给数组指定位置赋值
                                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;           // Slot is now non-empty
                        }
                    }
                    collide = false;
                }
                else if (!wasUncontended)       // CAS already known to fail
                    wasUncontended = true;      // Continue after rehash
                //执行到这儿,说明定位到的counterCell一定不为null
                //通过CAS对count进行累加。
                //如果线程的数量很多,即使散列但是依旧存在多线程竞争的情况,这里依旧会失败。
                //如果依旧失败,则会去尝试扩容
                else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
                    break;
                //这里的判断,就是为了看是否还能扩容
                //如果counterCells != as,说明肯定已经有其他线程已经扩容完毕了,因为扩容会创建新的数组,那就不需要再进行扩容了
                //n >= NCPU,如果当前counterCells 数组的长度大于等于CPU的核数,也不能再继续扩容。
                else if (counterCells != as || n >= NCPU)
                    collide = false;            // At max size or stale
                else if (!collide)
                    collide = true;
               	//执行到这儿,说明散列后还存在竞争,并且还能够继续扩容
               	//通过CAS设置cellsBusy,保证只能有一个线程扩容,并且扩容期间不允许进行CountCells对应数组位置的CountCell初始化操作
                else if (cellsBusy == 0 &&
                         U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                    try {
                        if (counterCells == as) {// Expand table unless stale
                        //counterCells 的长度扩大为原来的两倍,
                        //将counterCell移到新的数组中,
                        //并用新的counterCells替换旧的counterCells 
                            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);
            }
            //对counterCells进行初始化,因为存在多线程,所以通过CAS设置状态值cellsBusy来保证只有一个线程能够初始化成功
            else if (cellsBusy == 0 && counterCells == as &&
                     U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                boolean init = false;
                try {                           // Initialize table
                    if (counterCells == as) {
                    	//初始化的过程,就是创建一个长度为2的CounterCell数组
                        CounterCell[] rs = new CounterCell[2];
                        //因为是第一初始化,直接给数组的位置创建一个CounterCell并赋值即可。
                        rs[h & 1] = new CounterCell(x);
                        counterCells = rs;
                        init = true;
                    }
                } finally {
                	//退出的时候恢复cellsBusy 
                    cellsBusy = 0;
                }
                if (init)
                    break;
            }
            else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
                break;                          // Fall back on using base
        }
    }

fullAddCount包含了CounterCell初始化、扩容、计数累加的功能。

(1)一开始,就先获取一个随机数,用于后面的散列操作。fullAddCount计数因为没有hash进行散列,所以他通过ThreadLocalRandom获取随机数的方式来散列实现多线程计数,对ThreadLocalRandom不太熟悉的,可以看 Random和ThreadLocalRandom原理分析

(2)如果counterCells为空,就会对其初始化,初始化之前通过CAS将cellsBusy设置为1,保证只有一个线程能进行初始化。counterCells的初始化长度为2.

(3)如果counterCells不为null,则开始计数。如果拿到的counterCells数组对应位置的counterCell为null,则需要初始化当前位置的counterCell。如果不为null,则直接通过CAS进行计数。

(4)当在用数组进行计数还是存在竞争导致CAS失败的情况下,判读当前的counterCells数组长度是否大于等于当前操作系统的CPU核数,如果不大于,则进行扩容,如果已经大于等于了,则只能通过不断的CAS完成最终的计数。

(5)addCount结束后,会通过sumCount计算总的count数量,用于后面的扩容判断,sunCount实现如下,就是一个累加的操作。

final long sumCount() {
    CounterCell[] as = counterCells; CounterCell a;
    long sum = baseCount;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    return sum;
}

5 扩容

在addCount之后,就会进行扩容的操作:

private final void addCount(long x, int check) {
   	//......
    if (check >= 0) {
    	//这里开始扩容的逻辑
    	//扩容的时候支持多个线程同时扩容,会使用SIZECTL记录当前参与扩容的线程的数量。
    	//使用SIZECTL的高16位作为扩容戳,扩容戳计算和当前tab长度有关,详细的看resizeStamp
    	//使用SIZECTL的第16位记录当前参与扩容的线程的数量。参与扩容的线程的数量 = 低16位 - 1
        Node<K,V>[] tab, nt; int n, sc;
        //在while里面为sc赋值了,sc = sizeCtl = 扩容阈值 = count * 0.75
        //只有当已经存放的数量 > 扩容阀值,才进行扩容操作
        //这里还有一点需要注意:因为支持多个线程同时进行扩容,而一开始扩容,sizeCtl的值一直将会是负数。
        //只有当扩容结束,sizeCtl才会被重新设置为扩容阀值。所以,想要通过while条件退出循环,必须等到扩容结束。
        while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
               (n = tab.length) < MAXIMUM_CAPACITY) {
            //这里拿到的rs,第16位一定为1
            int rs = resizeStamp(n);
            //第一次,sc一定不小于0,执行else
            //第一次执行else,在else中,会将其设置为负数。
            if (sc < 0) {
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                    transferIndex <= 0)
                    break;
                //继续有其他线程来扩容,为SIZECTL加1记录线程数量。
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                    transfer(tab, nt);
            }
            //因为会有多个线程同时进入到这里进行扩容,因为公用SIZECTL来记录当前有多少个线程参与扩容,所以要使用CAS修改SIZECTL
            //第一次进行的时候,设置SIZECTL = rs << RESIZE_STAMP_SHIFT) + 2。得到的一定是负数,需要看resizeStamp
            else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                         (rs << RESIZE_STAMP_SHIFT) + 2))
                //transfer是真正的扩容逻辑
                transfer(tab, null);
            s = sumCount();
        }
    }
}

(1)支持多线程同时扩容。

(2)扩容的时候使用的while循环中有条件s >= (long)(sc = sizeCtl),这个判断有三个作用:

  • 扩容前,sizeCtl就是之前计算的扩容阀值。当前的数量大于等于sizeCtl,才进行扩容操作。

  • 扩容中的时候,会将sizeCtl设置为负数,只有在扩容结束,sizeCtl才会被设置为新的阀值。所以扩容中,这个条件一直是成立的,保证了参与扩容的多个线程想要通过while条件退出循环,必须等到扩容结束,否则这些个线程会一直帮助扩容,知道扩容完成。

(3)开始扩容的时候,将sizeCtl设置为负数,其高16位表示扩容戳,扩容戳和tab数组长度相关。使用其低16为来记录当前有多少个线程正在帮助扩容。扩容中sizeCtl扩容的计算依赖于resizeStamp方法。

static final int resizeStamp(int n) {
	//numberOfLeadingZeros获取的是n的二进制的最高位到第一个非0之间,有多少个0。得到的结果范围在0 - 32之间.
	//和 1 << 15去或运算,得到的最终结果:其第16位一定为1。这个为了后面将其左移16位变成负数。
    return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}

(4)第一个线程来扩容的时候,记录参与扩容的线程数量为(rs << RESIZE_STAMP_SHIFT) + 2,即使用低16位记录参与扩容的线程数量。后面有线程来参与扩容,记录参与扩容的线程数量为(rs << RESIZE_STAMP_SHIFT) + 1。所以参与扩容的线程数量就是(rs << RESIZE_STAMP_SHIFT) - 1结果的低16位。

(5)真正的扩容,执行的是transfer

前面提到,ConcurrentHashMap支持多线程同时扩容,其实描述为支持多个线程同时迁移节点数据到扩容后的新的tab中更为准确。所以可想而知,为了实现多线程迁移数据,必须划定好迁移范围,每个线程迁移一定范围的数据。

//参数中:tab是原tab,nextTab是扩容后新的tab
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;
    //扩容过程由于tab长度变化,导致数据在tab中的位置也会发生变化,所以需要转移数据。
    //转移数据支持多个线程共同转移,所以要划分好每个线程转移那一部分。
    //stride就是用于定义每个线程转移tab的长度。
    //这里,如果CPU核数为1,则tride = n, 则只一个线程进行处理。
    //如果CPU核数大于1,则将数组分为 (CPU数 * 8)段。允许(CPU数 * 8)个线程处理。
    //但是,最小的线程处理单位长度为16
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range
    if (nextTab == null) {            // initiating
        try {
            @SuppressWarnings("unchecked")
            //如果是第一个线程来扩容,则创建一个新的tab,长度为原tab的2倍
            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
            nextTab = nt;
        } catch (Throwable ex) {      // try to cope with OOME
            sizeCtl = Integer.MAX_VALUE;
            return;
        }
        nextTable = nextTab;
        //这里给transferIndex 赋值为原tab的长度lenth
        //下面就会通过这个transferIndex分配每个线程的处理长度
        //后面分配处理的时候,用bound 和i分别指向范围的左端和右端
        //i = transferIndex - 1; bound = transferIndex - stride; transferIndex - bound
        //当然,这里也只有第一个来扩容的线程才会进入到这里进行赋值
        transferIndex = n;
    }
    int nextn = nextTab.length;
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    boolean advance = true;
    boolean finishing = false; // to ensure sweep before committing nextTab
   	//这里for循环里面,就是真正的迁移。
   	//给每个线程分配处理的范围,范围大小为前面计算的stride 。
   	//用bound 和i分别指向范围的左端和右端,用全局变量transferIndex来控制每个线程处理范围的不重复。
   	//分配好范围后,通过--i来遍历处理每一个Node,如果当前节点Node处理完成,将其置为fwd
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;
        //这里的while循环:线程第一次来的时候,用于分配处理范围。分配好之后,通过--i实现遍历处理
        //advance:开始处理当前的tab[i]的时候,advance会置为false
        //         当前的tab[i]处理完成的时候,advance会置为true
        while (advance) {
            int nextIndex, nextBound;
            //这里需要关注一下,--i就是用于遍历使用的。
            //如果线程第一次来,还没有分配范围,此时i = bound = 0,所以条件不满足,这里走else
            if (--i >= bound || finishing)
                advance = false;
            //如果这个if满足条件,说明,tab已经全部分配完了。
            else if ((nextIndex = transferIndex) <= 0) {
                i = -1;
                advance = false;
            }
            //执行到这里,说明当前线程第一次进入,还没有分配长度,这里就会就行长度分配
            //首先因为是多个线程同时进入,所以通过CAS修改transferIndex。修改成功,说明当前线程获得了此范围的转移权
            //用bound 和i分别指向范围的左端和右端
            else if (U.compareAndSwapInt
                     (this, TRANSFERINDEX, nextIndex,
                      nextBound = (nextIndex > stride ?
                                   nextIndex - stride : 0))) {
                bound = nextBound;
                i = nextIndex - 1;
                advance = false;
            }
        }
        //在前面的while中分配处理范围的时候,如果已经分配完了,则 i = -1
       	//所以这里,只有分配到的最后一个线程才会执行到这里
        if (i < 0 || i >= n || i + n >= nextn) {
            int sc;
            //finishing为true,说明已经全部转移完成了,则重新为sizeCtl 何 tab赋值。
            if (finishing) {
                nextTable = null;
                table = nextTab;
                sizeCtl = (n << 1) - (n >>> 1);
                return;
            }
            //每个参与扩容的线程,如果扩容结束,就将参与扩容的线程减一
            //只有当所有的参与扩容的线程全部执行结束,才会将finishing 设置为true
            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    return;
                finishing = advance = true;
                i = n; // recheck before commit
            }
        }
        else if ((f = tabAt(tab, i)) == null)
            advance = casTabAt(tab, i, null, fwd);
        else if ((fh = f.hash) == MOVED)
            advance = true; // already processed
        else {
        	//这里是转移每个槽位的逻辑,需要加锁
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    Node<K,V> ln, hn;
                    //说明此时还是链表
                    if (fh >= 0) {
                        int runBit = fh & n;
                        Node<K,V> lastRun = f;
                        for (Node<K,V> p = f.next; p != null; p = p.next) {
                            int b = p.hash & n;
                            if (b != runBit) {
                                runBit = b;
                                lastRun = p;
                            }
                        }
                        if (runBit == 0) {
                            ln = lastRun;
                            hn = null;
                        }
                        else {
                            hn = lastRun;
                            ln = null;
                        }
                        //这里是完全创建了新的节点,而非继续使用原节点
                        //这样做,保证了原tab不变,所以在扩容期间,依旧可以进行get。
                        for (Node<K,V> p = f; p != lastRun; p = p.next) {
                            int ph = p.hash; K pk = p.key; V pv = p.val;
                            if ((ph & n) == 0)
                                ln = new Node<K,V>(ph, pk, pv, ln);
                            else
                                hn = new Node<K,V>(ph, pk, pv, hn);
                        }
                        setTabAt(nextTab, i, ln);
                        setTabAt(nextTab, i + n, hn);
                        setTabAt(tab, i, fwd);
                        advance = true;
                    }
                    //说明已经是树结构,树结构不分析。
                    else if (f instanceof TreeBin) {
                        TreeBin<K,V> t = (TreeBin<K,V>)f;
                        TreeNode<K,V> lo = null, loTail = null;
                        TreeNode<K,V> hi = null, hiTail = null;
                        int lc = 0, hc = 0;
                        for (Node<K,V> e = t.first; e != null; e = e.next) {
                            int h = e.hash;
                            TreeNode<K,V> p = new TreeNode<K,V>
                                (h, e.key, e.val, null, null);
                            if ((h & n) == 0) {
                                if ((p.prev = loTail) == null)
                                    lo = p;
                                else
                                    loTail.next = p;
                                loTail = p;
                                ++lc;
                            }
                            else {
                                if ((p.prev = hiTail) == null)
                                    hi = p;
                                else
                                    hiTail.next = p;
                                hiTail = p;
                                ++hc;
                            }
                        }
                        ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                            (hc != 0) ? new TreeBin<K,V>(lo) : t;
                        hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                            (lc != 0) ? new TreeBin<K,V>(hi) : t;
                        setTabAt(nextTab, i, ln);
                        setTabAt(nextTab, i + n, hn);
                        setTabAt(tab, i, fwd);
                        advance = true;
                    }
                }
            }
        }
    }
}

(1)通过transferIndex来划分每个线程的迁移范围。迁移范围的大小stride通过CPU的核数和tab数组长度计算得出,stride最小为16。假设tab数组长度为32,stride为16,第一个线程来,就迁移[16, 31]区间范围的数据,第二个线程就迁移[0, 15]区间范围的数据。使用bound和i来确定范围[bound, i],通过–i来遍历每个区间的Node进行迁移。

if (U.compareAndSwapInt
        (this, TRANSFERINDEX, nextIndex,
          nextBound = (nextIndex > stride ?
                       nextIndex - stride : 0))) {
    bound = nextBound;
    i = nextIndex - 1;
    advance = false;
}

(2)每处理完一个tab[i]对应位置的数据的迁移,就将tab[i]设置为fwd节点,fwd节点的hash值为MOVE,说明当前tab[i]已经完成了数据迁移

ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);

(3)真正迁移每一条链的时候,ConcurrentHashMap并不是去遍历链中的每一个Node,重新通过hash & (n - 1)放入的新的tab中。而是通过hash & n将原链的Node区分并拼接成为低位链以及高位链,然后将高位链和低位链放入到新的tab指定的位置。这里具体说明一下:
假设当前的tab长度为16,现在tab[5]位置的链如下,并且此时在扩容迁移tab[5]的链。
在这里插入图片描述
区分高低链:如果Node.hash & n == 0则属于低位链,否则属于高位链。为什么要这样区分呢?这个和二进制相关:
hash是任意的,在使用hash计算tab的index的时候,如果目前tab长度为16和长度为32,计算如下:
在这里插入图片描述
从上面的计算过程看,可以看出:
当hash的从低到高第5位为0的时候,(hash & (32 - 1)) = (hash & (16 - 1))
当hash的从低到高第5位为1的时候,(hash & (32 - 1)) = (hash & (16 - 1)) + 16

所以,一条链上的节点迁移后,要么在原来的位置,要么会被迁移到原位置 + n的位置。而计算迁移后位置的关键,在于hash的从低到高第X位是否为0,而通过Node.hash & n可以判断出其第X位是否为0。所以:如果Node.hash & n == 0则属于低位链,否则属于高位链。

区分高低位并迁移的代码如下:

//第一步
int runBit = fh & n;
Node<K,V> lastRun = f;
for (Node<K,V> p = f.next; p != null; p = p.next) {
    int b = p.hash & n;
    if (b != runBit) {
        runBit = b;
        lastRun = p;
    }
}
if (runBit == 0) {
    ln = lastRun;
    hn = null;
}
else {
    hn = lastRun;
    ln = null;
}
//第二步
for (Node<K,V> p = f; p != lastRun; p = p.next) {
    int ph = p.hash; K pk = p.key; V pv = p.val;
    if ((ph & n) == 0)
        ln = new Node<K,V>(ph, pk, pv, ln);
    else
        hn = new Node<K,V>(ph, pk, pv, hn);
}
//第三部
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);

我讲上面的代码分为三步,第二步是循环遍历拼接高低链的,第三步是将拼接好的高低链一次性迁移到指定的位置。那第一步是干什么的?为什么要有第一步?

可以看一下ConcurrentHashMap的get方法就会发现,get方法并没有加锁:

public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    int h = spread(key.hashCode());
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null;
        while ((e = e.next) != null) {
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}

也就是在ConcurrentHashMap进行put或者迁移的过程中,都可以进行get操作。put的时候确实可以,因为put将Node追加在链表末端。迁移的时候,如果想做到不影响get,只能在迁移的过程中重新创建新的Node节点,从迁移过程中的第二步能看到,确实是这样做的。但是这样做必定会创建很多的Node节点,那怎样做才能尽力减少新的Node节点的创建呢?答案是:链表最后的几个迁移后在同一链(高低链)的Node可以不重新创建,可同时连接在新的tab和原tab中,这里有点绕,用图表示如下:

在这里插入图片描述
左边是原tab,长度为16,右边是新tab,长度为32,。经过迁移之后,两个tab的情况如图所以,只创建了4个新的Node,来链表的最后的3个Node是可以复用的。

所以在迁移过程中,第一步就是为了找出最后的那三个可以被复用的Node。

(4)迁移完成后,会重新设置tab的值,以及新的阀值sizeCtl

if (i < 0 || i >= n || i + n >= nextn) {
    int sc;
    if (finishing) {
        nextTable = null;
        table = nextTab;
        sizeCtl = (n << 1) - (n >>> 1);
        return;
    }
    if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
        if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
            return;
        finishing = advance = true;
        i = n; // recheck before commit
    }

6 Hash冲突

hash冲突就是通过hash值计算得到的index相同,此时需要将hash冲突的Node节点放到链表或者红黑树中。当然hash冲突后需要比较equals,如果equals,那就需要覆盖了value(默认是需要覆盖了,也可以通过参数指定)。

ConcurrentHashMap为了降低hash冲突的概率,会对hash值进行高低位交叉处理增加散列程度:

//高低位交叉,增加散列程度。
static final int spread(int h) {
    return (h ^ (h >>> 16)) & HASH_BITS;
}

7 为什么重新equals必须重写hashCode。

重新equals必须重写hashCode是为了保证equals相等的情况下hashcode必须也要相等,为什么要这样?

首先hashCode相等,equals可以不必相等。如果hashCode相等,会出现hash冲突,map通过链表和红黑树的方式进行解决。

但是,如果equals相同,hashcode不相等了,会出现什么问题呢?会导致不同链上存在equals相等的Node,如果在map扩容的过程中将这些Node放在了一条链上,那问题就来了,导致那个Node在前面,则get就会得到哪个Node,会导致在不同时间点上,可能多次get获得的值是不同的。

所以要保证equals相等的情况下hashcode必须也要相等,这样在put的时候,就会将equals相同的值进行覆盖。

8 扩容阀值

tab的长度为n,当map中存放的数据达到扩容阀值的时候需要扩容。扩容是为了防止某一条链或者树上的节点个数过多降低了效率。扩容阀值的计算是:

 sc = n - (n >>> 2)  等价于  sc = n * 0.75

9 什么情况下,链表会转换为红黑树

当一条链上的节点个数大于8的时候,就会将其转换为红黑树。但是在转换红黑树之前还会再判断,判断当前tab的长度是否大于64。如果小于64,仅扩容即可。

//如果链表的长度大于8,则会调用这个方法
private final void treeifyBin(Node<K,V>[] tab, int index) {
    Node<K,V> b; int n, sc;
    if (tab != null) {
    	//再转红黑树之前,先判断,此时tab的长度是否大于64。如果小于64,仅扩容即可。
        if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
            tryPresize(n << 1);
        else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
            synchronized (b) {
                if (tabAt(tab, index) == b) {
                    TreeNode<K,V> hd = null, tl = null;
                    //转红黑树的逻辑
                    //关于红黑树的逻辑,这里不分析
                    for (Node<K,V> e = b; e != null; e = e.next) {
                        TreeNode<K,V> p =
                            new TreeNode<K,V>(e.hash, e.key, e.val,
                                              null, null);
                        if ((p.prev = tl) == null)
                            hd = p;
                        else
                            tl.next = p;
                        tl = p;
                    }
                    //将红黑树根据节点,设置到tab指定位置
                    setTabAt(tab, index, new TreeBin<K,V>(hd));
                }
            }
        }
    }
}

10 什么情况下get/put会被阻塞

get:文中已经分析过,任何情况下都可以进行get操作。因为put的时候属于尾插,不影响get。而扩容转移链表数据的时候会创建新的Node,所以扩容过程中也不影响原链表。

put:put操作的时候会对tab[ i ]进行synchronized加锁,在对tab[i]进行put的时候,会阻塞tab[ i ]上的其他put操作,但是不影响tab[ j ]的put操作。
在扩容过程中迁移的时候,也会对tab[ i ]进行synchronized加锁,所以迁移过程中,也会阻塞当前正在迁移的tab[ i ]的put操作。

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值