最详细集合源码解析之ConcurrentHashMap集合源码解析(一)

205 阅读13分钟

   小知识,大挑战!本文正在参与「程序员必备小知识」创作活动

ConcurrentHashMap是HashMap的线程安全的版本,其内部使用的是数组+链表+红黑树的结构来存储元素。

带着问题来看源码,可能效果更好,所以开篇我先放几个问题,让大家带着思考去看源码

01

开篇问题

  1. ConcurrentHashMap与HashMap的数据结构是否一样

  2. ConcurrentHashMap如何解决并发安全问题

  3. ConcurrentHashMap使用了哪些锁

  4. ConcurrentHashMap扩容是如何进行的

  5. ConcurrentHashMap是否是强一致性的

  6. ConcurrentHashMap不能解决哪些问题

02

ConCurrentHashMap整体结构

图片

其继承结构和HashMap差不多,这里就不多介绍。先来看下其存储元素的结构图

图片

图的上半部分是ConcurrentHashmap在jdk1.8的存储元素的结构,下半部分是1.8之前存储元素的逻辑,相互对比,其中一个最主要的差异就是1.8之前是锁住一段数组的桶位去保证线程安全,而在1.8开始是锁住每一个桶位,这样效率就高很多,同时1.8开始运用到了红黑树来存储元素,效率有更进一步的提升。

03

属性分析

 /* ---------------- Constants -------------- */

    // 散列表数组最大限制
    private static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * 散列表默认初始值
     */
    private static final int DEFAULT_CAPACITY = 16;

    /**
     * 最大数组长度
     */
    static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    /**
     * 并发级别,jdk1.7遗留下来的,1.8只有在初始化的时候用了一用
     * 不代表并发级别
     */
    private static final int DEFAULT_CONCURRENCY_LEVEL = 16;

    /**
     * 负载因子,在jdk1.8中,ConcurrentHashMap是固定值,不能改变
     */
    private static final float LOAD_FACTOR = 0.75f;

    /**
     * 转为红黑树的阈值,链表长度达到8,有可能发生树化操作
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * 红黑树转化为链表的阈值
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * 结合TREEIFY_THRESHOLD控制桶位是否树化,只有当table数组长度达到64,
     * 且某个桶位中链表长度达到8才会发生树化
     */
    static final int MIN_TREEIFY_CAPACITY = 64;

    /**
     * 当进行扩容时,可能会有多个线程参与,用来给每个线程分配区间
     * 线程迁移数据最小步长,控制线程迁移任务最小区间的一个值
     */
    private static final int MIN_TRANSFER_STRIDE = 16;

    /**
     * 扩容相关,计算扩容时生成的一个标识戳
     */
    private static int RESIZE_STAMP_BITS = 16;

    /**
     * 65535 代表并发扩容最多线程数
     */
    private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;

    /**
     * 扩容相关
     */
    private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;

    /*
     * Encodings for Node hash fields. See above for explanation.
     */
    // 当node节点的hash值为-1 时,表示当前节点正在扩容转移数据
    static final int MOVED = -1// hash for forwarding nodes
    //  当node节点的hash值为-2 时,表示当前节点已经树化,且当前节点
    // 为TreeBin对象,TreeBin对象代理换做红黑树
    static final int TREEBIN = -2// hash for roots of trees
    static final int RESERVED = -3// hash for transient reservations
    // 0x7fffffff ->0111 1111 1111 1111 1111 1111 1111 1111 可以将一个
    // 负数通过位与运算得到正数,但不是取绝对值
    static final int HASH_BITS = 0x7fffffff// usable bits of normal node hash

    /**
     * 当前系统CPU数量
     */
    static final int NCPU = Runtime.getRuntime().availableProcessors();

    /**
     * 散列表,长度一定是2的次方
     */
    transient volatile Node<K, V>[] table;

    /**
     * 扩容过程中,会将扩容中的table 赋值给nextTable 保持引用
     * 扩容结束后,这里会被设置为null
     */
    private transient volatile Node<K, V>[] nextTable;

    /**
     * LongAddr中的baseCount未发生竞争时或者当前LongAdder处于加锁状态时
     * 增量累计到baseCount中
     */
    private transient volatile long baseCount;

    /**
     * sizeCtl <0
     * 1.  -1表示当前table数组正在初始化(有线程再创建table数组),当前线程需要自旋等待
     * 2.   表示当前table正在扩容 高16位表示: 扩容的标识戳 低16位表示(1+nThread) 当前参与并发扩容的线程数量
     * sizeCtl=0 ,表示创建table数组时,使用DEFAULT CAPACITY为大小
     * sizeCtl>0
     * 1. 如果table未初始化,表示初始化大小
     * 2. 如果table已经初始化,表示下次扩容时的触发条件(阈值)
     */
    private transient volatile int sizeCtl;

    /**
     * 扩容过程中,记录当前进度,所有线程都需要从transFerIndex中分配区间任务
     * 去执行自己的任务
     */
    private transient volatile int transferIndex;

    /**
     * LongAdder中的cellsBusy 0表示当前LongAdder对象无锁状态
     * 1表示当前LongAdder对象加锁状态
     */
    private transient volatile int cellsBusy;

    /**
     * LongAdder中的cells数组,当baseCount发生竞争后,会创建cells数组
     * 线程会通过计算hash值 取到自己的cell,将增量累计到指定的cell中
     * 总数= sum(cells)+basecount
     */
    private transient volatile CounterCell[] counterCells;

    /**
     * concurrencyLevel代表并发级别,是为了兼容1.7
     */
    public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
        if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
        //如果传的初始化容量小于并发级别就将并发级别赋值给初始化容量
        if (initialCapacity < concurrencyLevel)   // Use at least as many bins
            initialCapacity = concurrencyLevel;   // as estimated threads
        /**计算size,假设传进来的初始化容量是16,负载因子是0.75
         * (long) (1.0 + (long) initialCapacity / loadFactor=23
         * cap : 32
         * */
        long size = (long) (1.0 + (long) initialCapacity / loadFactor);
        int cap = (size >= (long) MAXIMUM_CAPACITY) ?
                MAXIMUM_CAPACITY : tableSizeFor((int) size);
        //当 sizeCtl大于0,且 当前table未初始化,sizeCtl表示初始化容量
        this.sizeCtl = cap;
    }

这些属性中有个要重点强调下,sizeCtl,这个属性贯穿整个ConcurrentHashMap,也很重要,其不同的值代表不同的含义。

1 sizeCtl>0

如果table未初始化,表示初始化大小,如果已经初始化代表下次扩容的阈值

2 sizeCtl=0

表示创建table数组时,使用DEFAULT CAPACITY为大小

3 sizeCtl<0

等于-1,表示当前数组正在初始化

不等于-1, 表示当前table正在扩容 高16位表示: 扩容的标识戳 低16位表示(1+nThread) 当前参与并发扩容的线程数量。

04

put方法

   public V put(K key, V value) {
        // onlyIfAbent是false,代表如果key相等就做替换操作
        //false:key相等就插入不进去
        return putVal(key, value, false);
    }

    /**
     * Implementation for put and putIfAbsent
     */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        // 通过spread方法,可以让高位参与寻址运算
        int hash = spread(key.hashCode());
        // binCount标识当前k-v 封装成node后插入到指定桶位后
        //在桶位中所属链表的下标位置
        //0 表示当前桶位为NULL,node可以直接放
        //2 表示当前桶位可能是红黑树
        int binCount = 0;
        for (Node<K, V>[] tab = table; ; ) {
            // f:当前桶位头结点
            // n: 表示散列表数组长度
            // i: 表示key通过寻址计算后,得到的桶位下标
            // fh: 表示桶位头结点的hash值
            Node<K, V> f;
            int n, i, fh;
            // CASE1: 成立, 表示当前map中的table尚未初始化。。
            if (tab == null || (n = tab.length) == 0)

                tab = initTable();
                // CASE2: 成立,i表示key使用路由寻址算法得到key对应table数组的下标位置
                // tableAt 获取指定桶位的头结点
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                // cas方式插入数据,四个参数依次是当前数组,要插入位置数组下标,期望值,新值
                if (casTabAt(tab, i, null,
                        new Node<K, V>(hash, key, value, null)))
                    break;

                //CASE3:成立,表示当前桶位的头结点为FWD节点,表示目前map
                // 正在处于扩容
            } else if ((fh = f.hash) == MOVED)
                // 看到fwd结点后,当前节点有义务帮助当前map对象完成迁移数据的工作

                tab = helpTransfer(tab, f);
                //CASE4 :成立,当前桶位可能是链表,也可能是红黑树代理结点TreeBin
            else {
                //当前key存在时,会将旧值赋值给oldVal,返回给put方法调用处
                V oldVal = null;
                // 使用sync 加锁 “头节点” ,理论上是“头节点”
                synchronized (f) {
                    // 为什么又要对比一下,看看当前桶位的头节点,是否为之前获取的头节点?
                    // 是为了避免其他线程将该桶位的头节点修改掉,导致当前线程从sync加锁就有问题了,之后所有操作就不用做了

                    //条件成立,说明加锁对象没有问题,可以进来操作了
                    if (tabAt(tab, i) == f) {
                        // 条件成立,说明当前桶位就是普通链表桶位
                        if (fh >= 0) {
                            // 1. 当前插入key与链表当中所有元素的key都不一致时
                            //当前的插入操作是追加到链表的末尾,binCount表示链表长度
                            // 2. 当前插入key与链表当中的某个元素的key一致时,当前插入操作可能就是替换了
                            // binCount表示冲突位置(binCount-1)
                            binCount = 1;
                            // 迭代循环当前桶位的链表,e是每次循环处理节点
                            for (Node<K, V> e = f; ; ++binCount) {
                                // 当前循环节点key
                                K ek;
                                //条件一:e.hash==hash成立,表示循环的当前元素的hash值与插入节点的hash值一致,需要进一步判断
                                // 条件二:成立,说明循环的当前节点与插入节点的key一致,发生冲突了

                                if (e.hash == hash &&
                                        ((ek = e.key) == key ||
                                                (ek != null && key.equals(ek)))) {
                                    //将当前循环的元素的值赋值给oldVal
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                // 当前元素与插入元素的key不一致时,会走下面程序
                                // 1. 更新循环处理节点为当前节点的下一个结点
                                // 2. 判断下一个结点是否为null,如果是null,说明当前节点已经是队尾,插入数据需要追加到队尾节点的后面
                                Node<K, V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K, V>(hash, key,
                                            value, null);
                                    break;
                                }
                            }
                        }
                        //前置条件:该桶位一定不是链表
                        // 条件成立,表示当前桶位是红黑树代理结点TreeBin
                        else if (f instanceof TreeBin) {
                            // p 表示红黑树中如果与你插入节点key有冲突节点,则putTreeVal方法会返回冲突节点的引用
                            Node<K, V> p;
                            // 强制设置binCount=2,因为binCount<=1时还有其他含义,
                            binCount = 2;
                            // 成立:说明当前插入节点的key与红黑树中的某个节点的key一致,冲突了
                            if ((p = ((TreeBin<K, V>) f).putTreeVal(hash, key,
                                    value)) != null) {
                                // 将冲突节点的值赋值给oldVal
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                // 说明当前桶位不为null,可能是红黑树,也可能是链表
                if (binCount != 0) {
                    //如果binCount>=8,表示处理的桶位一定是链表
                    if (binCount >= TREEIFY_THRESHOLD)
                        //调用转化链表为红黑树的方法
                        treeifyBin(tab, i);
                    // 说明当前线程插入的key域原有key冲突,需要返回原有的value
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        // 1.统计当前 table 一共有多少数据
        // 2.判断是否达到扩容阀值标准,触发扩容
        addCount(1L, binCount);
        return null;
    }

整体流程跟HashMap比较类似,大致分为以下几步

1 如果数组未初始化,进行初始化

首先通过spread方法进行h对key的hash值进行扰动计算,这个和hashMap基本一致

tab为空进行初始化,调用initTable方法进行初始化,这个方法在后面会单独讲解

2 待插入元素所在桶位为空

如果待插入位置为空,就通过cas机制尝试将元素直接插入到桶位头结点

3 判断map是否正在进行扩容

如果当前的桶位头结点hash值为-1,则代表当前桶位是FWD节点,当前线程需要帮忙一起迁移元素,调用helpTransfer方法

4  如果待插入元素所在桶位不为空,并且没有迁移元素,则锁住这个桶位头结点

通过synchronized 来加锁,然后完成元素插入的工作

5  如果当前桶中元素是以链表方式进行存储,则在链表中寻找该元素或者插入元素

如何判断桶位是链表呢,就是判断桶位头结点hash值是否大于等于0,是的话就是链表。然后就是常规的for循环遍历寻找元素,没有的话就在链表尾部添加元素。

6  如果当前桶中元素是以红黑树方式存储,则在红黑树中寻找该元素或者插入元素

7  插入操作完成后判断链表长度是否超过8,超过的话就调用treeifyBin方法进行树化操作

8  最后对整个map元素个数加1,并检查是否需要扩容

调用addCount方法来完成对元素加1及扩容操作,这里就用到了我上篇文章写得LongAdder类,计数就是通过这个类完成,计数完成后然后判断是否需要扩容;

整个put方法主要用到了cas和synchronized这两种锁,为什么要用synchronized而不是ReentrantLock呢,在jdk1.6之前可能synchronized效率不是很高,但现在已经做了很多的优化,在某些情况下其实不比ReentrantLock差,这个synchronized会在后期文章单独讲解。

05

initTable方法

   private final Node<K, V>[] initTable() {
        // tab:引用map.table
        Node<K, V>[] tab;
        // sizeCtl的临时值
        int sc;
        // 自旋条件:map.tale 尚未初始化
        while ((tab = table) == null || tab.length == 0) {

            if ((sc = sizeCtl) < 0)
                // 大概率是-1,表示其他线程正在进行创建table的过程,当前线程没有竞争到初始化table的锁
                Thread.yield(); // lost initialization race; just spin

                // 1. sizeCtl=0,表示创建table数组时,使用DEFAULT_CAPCITY大小
                // 2.如果table未初始化,表示初始化大小
                // 3. 如果table已经初始化,表示下次扩容时的触发条件
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {

                try {
                    // 这里为什么又要判断呢?防止其他线程已经初始化完毕看,然后当前线程
                    // 再次初始化,导致丢失数据
                    //  条件成立。说明其他线程都没有进入过这个if块,当前线程就具备
                    // 初始化table权利了
                    if ((tab = table) == null || tab.length == 0) {
                        // sc大于0,创建table使用sc做为指定大小,否则使用16默认值
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node<K, V>[] nt = (Node<K, V>[]) new Node<?, ?>[n];
                        table = tab = nt;
                        // n>>>2=0.25n
                        //sc=0.75n,表示再次扩容的触发条件
                        sc = n - (n >>> 2);
                    }
                } finally {
                    //1. 如果当前线程是第一次创建map.table的线程的话,sc表示的是下一次扩容的阈值
                    // 2. 表示当前线程并不是第一次创建map.table的线程,当前线程进入到else if块时,
                    // 将sizeCtl设置为-1,那么这时需要将其修改为进入时的值
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

1  进行自旋,判断sizeCtl是否小于0

sie=zeCtl如果小于0,那么就有两种可能,要么是数组正在初始化,要么是数组正在扩容,在这里,大概率是-1,就是数组在初始化,如果数组在初始化,表示其他线程正在创建table,当前线程没有竞争到初始化table的锁

2  通过cas操作获取初始化table的锁

初始化操作,table的默认容量就是16,初始化完成,sizeCtl就赋值为0.75n,代表扩容的阈值。

06

addCount方法

addCount方法我们在put方法代码最后可以看到有调用,主要就是完成两个操作,一个是计数,一个是扩容,先来看下源码。

    private final void addCount(long x, int check) {
        //as 表示 LongAdder.cells
        //b 表示LongAdder.base
        //s 表示当前map.table中元素的数量
        CounterCell[] as;
        long b, s;
        // cells数组不为空或者线程写入base时竞争失败,
        if ((as = counterCells) != null ||
                !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
            CounterCell a;
            long v;
            int m;
            boolean uncontended = true;
            // as == null || (m = as.length - 1) < 0  cells数组还没有初始化,需要进行初始化
            //  = as[ThreadLocalRandom.getProbe() & m]) == null:当前线程命中的cell为空,需要new一个Cell
            //uncontended =U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x:
            // 表示当前线程使用cas方式更新当前命中的cell失败,需要重试或者扩容cells数组
            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(x, uncontended);
                return;
            }
            if (check <= 1)
                return;
            //获取当前散列表元素个数,这是一个期望值
            s = sumCount();
        }

        // 表示put操作调用的addCount方法
        if (check >= 0) {
            //tab 表示map.table
            //nt 表示map.nextTable
            //n 表示map.table数组的长度
            //sc 表示sizeCtl的临时值
            Node<K, V>[] tab, nt;
            int n, sc;

            /**
             * sizeCtl < 0
             * 1. -1 表示当前table正在初始化(有线程在创建table数组),当前线程需要自旋等待..
             * 2.表示当前table数组正在进行扩容 ,高16位表示:扩容的标识戳   低16位表示:(1 + nThread) 当前参与并发扩容的线程数量
             *
             * sizeCtl = 0,表示创建table数组时 使用DEFAULT_CAPACITY为大小
             *
             * sizeCtl > 0
             *
             * 1. 如果table未初始化,表示初始化大小
             * 2. 如果table已经初始化,表示下次扩容时的 触发条件(阈值)
             */

            //自旋
            //条件一:s >= (long)(sc = sizeCtl)
            //       true-> 1.当前sizeCtl为一个负数 表示正在扩容中..
            //              2.当前sizeCtl是一个正数,表示扩容阈值
            //       false-> 表示当前table尚未达到扩容条件
            //条件二:(tab = table) != null
            //       恒成立 true
            //条件三:(n = tab.length) < MAXIMUM_CAPACITY
            //       true->当前table长度小于最大值限制,则可以进行扩容。
            while (s >= (long) (sc = sizeCtl) && (tab = table) != null &&
                    (n = tab.length) < MAXIMUM_CAPACITY) {
                // 获取扩容批次唯一标识符
                int rs = resizeStamp(n);
                //表示当前数组正在扩容中
                if (sc < 0) {
                    //条件一:(sc >>> RESIZE_STAMP_SHIFT) != rs
                    //      true->说明当前线程获取到的扩容唯一标识戳 非 本批次扩容
                    //      false->说明当前线程获取到的扩容唯一标识戳 是 本批次扩容
                    //条件二: JDK1.8 中有bug jira已经提出来了 其实想表达的是 =  sc == (rs << 16 ) + 1
                    //        true-> 表示扩容完毕,当前线程不需要再参与进来了
                    //        false->扩容还在进行中,当前线程可以参与
                    //条件三:JDK1.8 中有bug jira已经提出来了 其实想表达的是 = sc == (rs<<16) + MAX_RESIZERS
                    //        true-> 表示当前参与并发扩容的线程达到了最大值 65535 - 1
                    //        false->表示当前线程可以参与进来
                    //条件四:(nt = nextTable) == null
                    //        true->表示本次扩容结束
                    //        false->扩容正在进行中
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                            sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                            transferIndex <= 0)
                        break;
                    //前置条件:当前table正在执行扩容中.. 当前线程有机会参与进扩容。
                    //条件成立:说明当前线程成功参与到扩容任务中,并且将sc低16位值加1,表示多了一个线程参与工作
                    //条件失败:1.当前有很多线程都在此处尝试修改sizeCtl,有其它一个线程修改成功了,导致你的sc期望值与内存中的值不一致 修改失败
                    //        2.transfer 任务内部的线程也修改了sizeCtl。
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        //协助扩容,持有nextTable引用
                        transfer(tab, nt);
                }
                // 条件成立,说明当前线程是触发扩容的第一个线程,在transfer方法需要做一些扩容准备工作
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                        (rs << RESIZE_STAMP_SHIFT) + 2))
                    //触发扩容条件的线程 不持有nextTable
                    transfer(tab, null);
                s = sumCount();
            }
        }
    }

1  计算元素个数

元素个数存储方式基本和LongAdder一致,看过我上面文章的这里应该都会清楚,这里我就不再进行讲解,不知道计数原理的看下我上篇文章就清楚了。

2  协助扩容

如果当前sizeCtl小于0或者当前容量大于sizeCtl,说明需要扩容,如果sizeCtl小于0,就代表当前正在处于扩容中,那么当前线程就会参与扩容,通过cas操作获取锁,并且sizeCtl会加1,这个加1就是将sizeCtl低16位加1,表示多了一个线程参与扩容,此时sizeCtl高位存储扩容邮戳(resizeStamp),低位存储扩容线程数加1(1+nThreads)

3 触发扩容

如果当前满足扩容条件,然后还没有进行扩容此时当前线程就会触发扩容操作

07

helpTransfer方法

/** * Helps transfer if a resize is in progress. */final Node<K, V>[] helpTransfer(Node<K, V>[] tab, Node<K, V> f) {    //nextTab 引用的是 fwd.nextTable == map.nextTable    //sc 保存map.sizeCtl    Node<K, V>[] nextTab;    int sc;    //条件一:tab != null 恒成立 true    //条件二:(f instanceof ForwardingNode) 恒成立 true    //条件三:((ForwardingNode<K,V>)f).nextTable) != null 恒成立 true    if (tab != null && (f instanceof ForwardingNode) &&            (nextTab = ((ForwardingNode<K, V>) f).nextTable) != null) {        //拿当前标的长度 获取 扩容标识戳   假设 16 -> 32 扩容:1000 0000 0001 1011        int rs = resizeStamp(tab.length);        //条件一:nextTab == nextTable        //成立:表示当前扩容正在进行中        //不成立:1.nextTable被设置为Null 了,扩容完毕后,会被设为Null        //       2.再次出发扩容了...咱们拿到的nextTab 也已经过期了...        //条件二:table == tab        //成立:说明 扩容正在进行中,还未完成        //不成立:说明扩容已经结束了,扩容结束之后,最后退出的线程 会设置 nextTable 为 table        //条件三:(sc = sizeCtl) < 0        //成立:说明扩容正在进行中        //不成立:说明sizeCtl当前是一个大于0的数,此时代表下次扩容的阈值,当前扩容已经结束。        while (nextTab == nextTable && table == tab &&                (sc = sizeCtl) < 0) {            //条件一:(sc >>> RESIZE_STAMP_SHIFT) != rs            //      true->说明当前线程获取到的扩容唯一标识戳 非 本批次扩容            //      false->说明当前线程获取到的扩容唯一标识戳 是 本批次扩容            //条件二: JDK1.8 中有bug jira已经提出来了 其实想表达的是 =  sc == (rs << 16 ) + 1            //        true-> 表示扩容完毕,当前线程不需要再参与进来了            //        false->扩容还在进行中,当前线程可以参与            //条件三:JDK1.8 中有bug jira已经提出来了 其实想表达的是 = sc == (rs<<16) + MAX_RESIZERS            //        true-> 表示当前参与并发扩容的线程达到了最大值 65535 - 1            //        false->表示当前线程可以参与进来            //条件四:transferIndex <= 0            //      true->说明map对象全局范围内的任务已经分配完了,当前线程进去也没活干..            //      false->还有任务可以分配。            if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||                    sc == rs + MAX_RESIZERS || transferIndex <= 0)                break;            if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {                transfer(tab, nextTab);                break;            }        }        return nextTab;    }    return table;}

只有当桶数组不为空,并且当前桶位头结点是ForwardingNode类型,并且nextTable不为空才有可能参与扩容。这个nextTable就是新桶数组。

有话说

ConcurrentHashMap挺复杂的,会分为两篇文章来讲,这里说下我学习源码的方法,学习源码一定不能只看书或者视频,一定要自己亲自去看源码,自己看源码看不懂的时候结合博客,视频去理解,这样才能理解的透彻,如果只是单纯看我文章,看文章的同时没有去结合代码,这样一篇文章下来其实学不到太多东西,也很容易遗忘。学习苦涩的,坚持就是胜利,现在看不到成果,日积月累必定会有你想象不到的回报。