ConcurrentHashMap源码逐行解析

简介:
因为博主前面写过HashMap的解析,所以这里只分析和HashMap不同点,类似的地方会略过,有需要的朋友可以结合HashMap的文章一起看。
不容易,终于把这篇文章更新出来了,ConcurrentHashMap实在是太难了,终于熬完了这锅鸡汤。one day day的,现在几乎可以背出来了。

在这里插入图片描述

废话不多说,跳过前戏,直接看代码(提醒一下,精华都是各种if判断)。
先列几个重要的参数:

    //阈值
    //=0:默认值
    //-1:表示正在初始化
    //<-1:表示多个线程在扩容(-2是第一个扩容线程,后面每多一个就+1)
    //低16位是扩容线程数量+1,因为第一个扩容线程是+2,高16位的最高位是1,剩下15位是数组的长度)
    //>1:表示需要扩容阈值
    private transient volatile int sizeCtl;
    //扩容时线程每次领取完还剩余的任务
    private transient volatile int transferIndex;
    //标志数组的下标正在扩容
    static final int MOVED     = -1;
    //标志数组的下标存放的是树结构
    static final int TREEBIN   = -2;

putVal第一部分

		//跟hashMap不一样的地方,不允许key为null
        if (key == null || value == null) throw new NullPointerException();
        //计算hash
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
    
    
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                //初始化
                tab = initTable();
                //计算下标,跟hashMap一样
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
    
    
                //跟hashMap不一样的地方,CAS新增头节点
                if (casTabAt(tab, i, null,
                        new Node<K,V>(hash, key, value, null)))
                    break;
            }

第一部分还是比较简单的:

  1. 计算hash
  2. 没有初始化就先初始化
  3. 根据hash算出下标,如果下标为null,就用CAS新增

spread():

这里的计算下标跟hashMap有点不一样,就是这里不允许hash为负数。

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

简单来说就是避免hash值是负数,具体原因下篇文章分析。

initTable():

      private final Node<K,V>[] initTable() {
    
    
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
    
    
              //sizeCtl刚开始是0,sizeCtl<0,说明有别的线程CAS成功了
            if ((sc = sizeCtl) < 0)
                //所以当前线程就先让出CPU时间片,等待下次的系统调度,再次走上面的while判断
                Thread.yield(); // lost initialization race; just spin
                //用CAS去修改SIZECTL为-1,表示在初始化,扩容是-2
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
    
    
                try {
    
    
					//双重校验
                    if ((tab = table) == null || tab.length == 0) {
    
    
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        sc = n - (n >>> 2);
                    }
                } finally {
    
    
                      //sizeCtl>0,就是扩容阈值
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

initTable的作用比较简单,就是初始化table的,但是要避免多线程初始化。
关于这里的双重校验额外说明一下:

仔细想想,这个双重校验不是为了这里,因为CAS是立即得到返回结果的,不是Lock或synchronized,不会阻塞
即便是多线程同时initTable也不会出现问题,这里是为了防止putAll的
在没有初始化的时候,一个线程put,一个线程putAll,就会出现问题。
putAll的初始化是调的tryPresize,这里的二次校验是防止tryPresize也在初始化

putVal第二部分

           	//判断hash是否为MOVED,就是判断是否有线程在扩容
            else if ((fh = f.hash) == MOVED)
                //帮助扩容
                tab = helpTransfer(tab, f);
            else {
    
    
                V oldVal = null;
                //跟hashMap不一样的地方,如果此下标已经有值了,锁住头节点
                synchronized (f) {
    
    
                    //获取该下标的值,用的getObjectVolatile,强制从主存中获取属性值
                    if (tabAt(tab, i) == f) {
    
    
                   		//判断是不是链表
                        if (fh >= 0) {
    
    
                            binCount = 1;
							//遍历链表,尾插
                            for (Node<K,V> e = f;; ++binCount) {
    
    
                                //...省略掉
                        }
                          //判断是红黑树结构
                        else if (f instanceof TreeBin) {
    
    
                            Node<K,V> p;
                            binCount = 2;
                            //...省略掉
                        }
                    }
                }
                  //binCount初始是0,0代表插入的下标没值,插入的是头节点
                  //binCount!= 0,1代表插入的是链表,2代表是树
                  //所以先判断binCount!= 0,再去判断是否达到转树的阈值
                if (binCount != 0) {
    
    
                    if (binCount >= TREEIFY_THRESHOLD)
                          //链表转树
                        treeifyBin(tab, i);
                    if (oldVal != null)
                          //说明是替换值,直接return了,下面的扩容不需要判断了
                        return oldVal;
                    break;
                }
            }
        }
          //数量+1,并且判断是否需要扩容
        addCount(1L, binCount);
        return null;
    }

这里就是精华了:

  1. 判断是否正在扩容,是的话就帮助扩容
  2. 插入链表或者Tree里面去
  3. 判断是否需要链表转Tree,或者是否需要替换旧值(treeifyBin)
  4. 总数量+1,且判断是否需要扩容(addCount)

treeifyBin():

    private final void treeifyBin(Node<K,V>[] tab, int index) {
    
    
        Node<K,V> b; int n, sc;
        if (tab != null) {
    
    
            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;
                          //这里遍历链表,把Node转成TreeNode,跟hashMap一样
                        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;
                        }
                          //将新的TreeNode改成Tree结构
                        setTabAt(tab, index, new TreeBin<K,V>(hd));
                    }
                }
            }
        }
    }

treeifyBin(Node<K,V>[] tab, int index)的作用:

  1. 判断总数是否小于64,小于就先扩容,不转Tree
  2. 遍历链表,把Node转成TreeNode
  3. 把TreeNode改成Tree结构

tryPresize():

        private final void tryPresize(int size) {
    
    
          //判断是否大于最大容量的一半,是的话就取最大容量,否则就取扩容2倍之后的最小的2的幂次方
        int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
                tableSizeFor(size + (size >>> 1) + 1);
        int sc;
        while ((sc = sizeCtl) >= 0) {
    
    
            Node<K,V>[] tab = table; int n;
              //判断是不是要初始化
            if (tab == null || (n = tab.length) == 0) {
    
    
                n = (sc > c) ? sc : c;
                  //用CAS去修改SIZECTL为-1,表示在初始化,扩容是-2
                if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
    
    
                    try {
    
    
                          //二次校验,同样,校验的不是这里,是initTable那边的初始化
                        if (table == tab) {
    
    
                            @SuppressWarnings("unchecked")
                            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                            table = nt;
                            sc = n - (n >>> 2);
                        }
                    } finally {
    
    
                        sizeCtl = sc;
                    }
                }
            }
              //这里是判断是否需要扩容
              //c <= sc:判断的是putAll丢进来的集合数量是否小于扩容阈值
              //n >= MAXIMUM_CAPACITY:判断的是数组长度是否大于最大容量
            else if (c <= sc || n >= MAXIMUM_CAPACITY)
                break;
              //走到这,说明要扩容
            else if (tab == table) {
    
    
                  //这里的判断跟addCount方法的判断一致,在addCount会详细说明
                int rs = resizeStamp(n);
                //省略掉
                .....
            }
        }
    }

tryPresize(int size):其实还挺简单的,判断要初始化还是扩容,扩容的判断细节留到addCount里面详细讲解。

TreeBin():

		TreeBin(TreeNode<K,V> b) {
    
    
			//注意这一步,把Node的hash改成TREEBIN,也就是是-2,所以判断 < 0就知道是Tree结构了
            super(TREEBIN, null, null, null);
            this.first = b;
            TreeNode<K,V> r = null;
            //红黑树的新增,跟hashMap,感兴趣的朋友可以去看博主写的hashMap篇
            for (TreeNode<K,V> x = b, next; x != null; x = next) {
    
    
           		//省略掉
                .....
            }
            this.root = r;
            assert checkInvariants(root);
       	}

TreeBin(TreeNode<K,V> b):也很简单,比起hashMap,就多了一步,把Nodehash改成-2用来区分链表和红黑树的。

addCount():

	private final void addCount(long x, int check) {
    
    
        CounterCell[] as; long b, s;
        //判断CAS是否失败
         if ((as = counterCells) != null ||
            !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
    
    
            //省略掉
            .....
        }
        //判断需不需要扩容
        if (check >= 0) {
    
    
            //省略掉
            .....
        }  
    }

也是相当复杂的,一步步看,先看下大概思路:

  1. 先判断有没有线程CAS增加总数量失败的,或者说自己CAS失败了,有就去记录
  2. 判断需不需要扩容
    好,简简单单的2步,但是复杂的令人想吐。在这里插入图片描述

判断CAS是否失败:

这部分就留到下篇文章(细节篇)里面去分析,因为不是很重要。这里就不影响大家的思路了,接着看高潮部分,扩容(下篇文章跟这篇会同步更新)

判断需不需要扩容:

		//判断是否是新增,删除是-1
        if (check >= 0) {
    
    
            Node<K,V>[] tab, nt; int n, sc;
            //判断是否需要扩容,sizeCtl是扩容阈值
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                    (n = tab.length) < MAXIMUM_CAPACITY) {
    
    
                //n是数组的长度,计算结果的低15位就是n,只不过第16位是1而已
                //n最小是16,最大 < MAXIMUM_CAPACITY
                //那么rs的范围就是 32795 ~ 32769,大概率是10000000000xxxxx
                int rs = resizeStamp(n);
                //判断是否有别的线程在扩容
                if (sc < 0) {
    
    
                	//极端情况的判断
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;
                    //SIZECTL + 1 ,其实就是低16位+1
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                    	//扩容
                        transfer(tab, nt);
                }
                //这一步就是当前没有线程在扩容,CAS去标志
                //此时SIZECTL的低16位是2,高16位的最高位是1,剩下15位是数组的长度
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
                s = sumCount();
            }
        }

resizeStamp():

    static final int resizeStamp(int n) {
    
    
        return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
    }

Integer.numberOfLeadingZeros(n):

Integer.numberOfLeadingZeros(n)是返回高位的所有的0的个数,具体代码就不分析了,反正就是一顿操作
Java的int类型是32位的
假设n = 2,二进制就是10,那么Integer.numberOfLeadingZeros(n) = 30
n = 20,二进制就是10100,那么Integer.numberOfLeadingZeros(n) = 27

1 << (RESIZE_STAMP_BITS - 1):

RESIZE_STAMP_BITS = 16
1 << (RESIZE_STAMP_BITS - 1) = 1000000000000000,是16

Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1)):

n是数组的长度,最小16,最大MAXIMUM_CAPACITY(1 << 30)
那么Integer.numberOfLeadingZeros(n)的结果就是最小是1,最大是27
( 1 ~ 27 ) | 1000000000000000
计算的结果就是低15位肯定是Integer.numberOfLeadingZeros(n),只不过第16位是1而已

一顿操作,返回值高16位全是0,第16位是1,低15位就是( 1 ~ 27 )之间的值。

第一个复杂点来了,关于已有线程在扩容,协助帮助扩容的逻辑 ,看下这几个极端情况的判断:

  				//判断是否有别的线程在扩容
                if (sc < 0) {
    
    
                      //就是这里的判断
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                            sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                            transferIndex <= 0)
                          //出现上述的情况,就跳出循环
                        break;
                }

再强调一下上面的逻辑是判断是否帮助扩容,而第一个线程进transfer之后:

sizeCtl:低16位是2,高16位的最高位是1,剩下15位是数组的长度,最高位是1,表示是负数

看下帮助扩容的逻辑:

if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                            sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                            transferIndex <= 0)
 

判断1:(sc >>> RESIZE_STAMP_SHIFT) != rs:

rs:低15位就是数组长度,只不过第16位是1而已,大概率是10000000000xxxxx,高16位都是0,正数
RESIZE_STAMP_SHIFT:16
sc < 0,sc = sizeCtl(16位是2,高16位的最高位是1,剩下15位是数组的长度)
sc >>> 16:就回到了resizeStamp(n)的值,也就是得到高16位
rs = resizeStamp(n),理论上应该是相等的
这里判断不相等,只可能是sc的高16位变化了,而高16代表了数组的长度,意味着扩容成功了 

判断2:sc == rs + 1:

sc == rs + 1:因为sc是负数,rs是正数,不可能相等  

判断3:sc == rs + MAX_RESIZERS:

MAX_RESIZERS = 65535,sc是负数,rs是正数,MAX_RESIZERS 是正数,sc == rs + MAX_RESIZERS,不可能

关于这2点我想了很久,各种查资料,发现有大佬已经向JDK提出了这个Bug,且被收录了,JDK bug link:https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8214427
上面2个判断应该改成:
判断2:sc == rs << 16 + 1:

rs << 16 + 1:左移16位之后,+1,那么低16位就是1,而sc的低16表示的扩容线程数
这里判断相等,就是判断的扩容是否结束扩容,第一个扩容线程的低16+2,后面每结束一个是-1,最后应该是1

判断3:sc == rs + MAX_RESIZERS:

跟判断2的思路一样,只不过这里是判断扩容线程是否达到上限

判断4:(nt = nextTable) == null:

nextTable只有是扩容的时候才有值, == null也表示扩容结束

判断5:transferIndex <= 0:

transferIndex 只会 == 0,表示最后一个任务已经被领取了,具体实现在扩容方法里面会详解

这几个判断属实牛逼,Doug Lea牛逼,经常出现一个值表示多种状态的,大佬就是大佬。

transfer():

好了,判断结束了,终于到核心中的核心了,扩容。
同样的,由于较为复杂,先说下大概思路,再show代码:

  1. 分配任务
  2. 领取任务
  3. 真正扩容
  4. 扩容结束的善后

分配任务

	private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    
    
        int n = tab.length, stride;
          //用CPU数量去判断,每个CPU扩容的数组长度,最少16
          //为什么是(n >>> 3) / NCPU,而不是直接n / NCPU呢?
          //我觉得大概是因为(个人想法):
          //1:真正扩容的线程一次不需要领取太多任务,给扩容线程一个机会,让别的线程有机会帮助扩容
          //2:尽可能的利用每个CPU,这样put场景多的情况,就会分散压力到每个CPU
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; // subdivide range
          //刚开始扩容,nextTab == null
        if (nextTab == null) {
    
    
            try {
    
    
                  //扩容2倍
                Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
                nextTab = nt;
            } catch (Throwable ex) {
    
    
                  //上面的代码会报错,原因只有2个
                  //1:n << 1结果过大,但是 < Integer.MAX_VALUE,导致OOM,堆空间溢出
                  //2:n << 1结果超出Integer.MAX_VALUE,导致结果是负数
                sizeCtl = Integer.MAX_VALUE;
                return;
            }
            nextTable = nextTab;
            //这里transferIndex是老数组的长度
            transferIndex = n;
        }
        

第一部分很简单,就是把数组分成几组,最少16个一组,线程一次扩容一组。

领取任务

	int nextn = nextTab.length;
      //定义一个ForwardingNode,并把hash变成了MOVED
      //当线程发现此下标的hash是MOVED,就知道了,这个下标正在扩容,会帮助扩容
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
      //标志位,用来标志这个线程需要处理的数组的下标,通过--i,一直遍历
    boolean advance = true;
      //标志位,用来退出
    boolean finishing = false; // to ensure sweep before committing nextTab
      //i:是遍历的下标
      //bound:表示当前线程需要处理的区间的最小下标(先找到最小下标,然后往后遍历扩容)
    for (int i = 0, bound = 0;;) {
    
    
        Node<K,V> f; int fh;
        while (advance) {
    
    
            int nextIndex, nextBound;
              //判断当前线程领取的任务有没有做完呢,比如说领取了16个,此时的bound为剩余的任务数量
              //循环每次--去判断,没完成,就直接false跳出这个循环,继续去干活
            if (--i >= bound || finishing)
                advance = false;
              //transferIndex == 0的时候,表示最后一份任务被领取了
              //而走到这个else if说明--i < bound,表示最后一份任务完成了,就会进这个if
            else if ((nextIndex = transferIndex) <= 0) {
    
    
                  //将 i 标志位 -1,下面有用
                i = -1;
                advance = false;
            }
              //用CAS去标志TRANSFERINDEX,成功即代表该线程领取任务成功
              //并把TRANSFERINDEX更新成剩下的区间,最后一份任务是0,这里对应着上面进扩容逻辑判断5
            else if (U.compareAndSwapInt
                    (this, TRANSFERINDEX, nextIndex,
                            nextBound = (nextIndex > stride ?
                                    nextIndex - stride : 0))) {
    
    
                  //把bound改为剩余的任务数量
                bound = nextBound;
                  //i = 剩余任务数量 -1,就是剩下数组的下标最大值
                i = nextIndex - 1;
                advance = false;
            }
        }

第二部分看上去挺复杂,接下来也不难。就是遍历数组,然后死循环的去用CAS更新transferIndex。更新成剩余任务。比如原来数组64,任务每组16个,就把transferIndex更新成48,最后一份就是更新成0。
几个判断也都有详细注释,判断了每组任务做完没有?任务领完没有?

真正扩容

    		//获取该下标的值,用的getObjectVolatile,强制从主存中获取属性值
            else if ((f = tabAt(tab, i)) == null)
                //如果为null就用fwd标志当前下标
                advance = casTabAt(tab, i, null, fwd);
            //到这说明当前下标不是null,并且有线程处理过了
            else if ((fh = f.hash) == MOVED)
                advance = true; 
            else {
    
    
                //锁住下标的头节点
                synchronized (f) {
    
    
                    //二次判断一下是否相等
                    if (tabAt(tab, i) == f) {
    
    
                        //如果不需要改变下标:ln和hn其中一个是null,一个是原来的链表
                        //如果需要改变下标:ln和hn其中一个是正反一半的链表,一个是反向链表
                        Node<K,V> ln, hn;
                        //判断是不是链表
                        if (fh >= 0) {
    
    
                            //计算值,用来判断是否需要改变下标,跟hashMap一样
                            int runBit = fh & n;
                            //lastRun要么是头节点,可以全部挪到新数组
                            //要么是需要拆分的其中一个链表的尾节点
                            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是另一个链表的头节点
                                    lastRun = p;
                                }
                            }
                            //为0则代表不需要改变下标
                            if (runBit == 0) {
    
    
                                ln = lastRun;
                                hn = null;
                            }
                            else {
    
    
                                hn = lastRun;
                                ln = null;
                            }
                            //同样是遍历,但是多了个p != lastRun:用来判断整个链表是否需要拆分的
                            //只有走到上面的for里面的if里面,才会不一样
                            //反之就是一样的,如果一样的,这个for就不会走了
                            //这里就是为啥遍历2次的原因(减少new Node的次数):
                            //判断第一个for是否可以直接把链表挪到新数组,就不需要new了
                            //需要变成2个链表,lastRun后面的同下标的节点不需要new了
                            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有值,那么这个链表就是一个反向链表,hn就是一个正向链表
                                    ln = new Node<K,V>(ph, pk, pv, ln);
                                else
                                    //同样
                                    hn = new Node<K,V>(ph, pk, pv, hn);
                            }
                            //ln挂到新数组的原下标
                            setTabAt(nextTab, i, ln);
                            //hn挂到新数组的原下标 + 老数组长度,跟HashMap一样
                            setTabAt(nextTab, i + n, hn);
                            //用fwd占位,进去遍历下一个下标
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                        //红黑树的扩容,跟HashMap一样,其实也是复制的链表,Tree同样维护了一份链表
                        else if (f instanceof TreeBin) {
    
    
                            //...省略掉
                        }
                    }
                }

第三部分,其实真的走到扩容这一步,反而很简单的,难的是上面的各种判断。就是很简单的遍历链表(红黑树结构也是遍历的链表),然后判断每个节点是否要挂到新的下标,然后CAS挂到新下标,基本上跟跟HashMap一样。
有一个小小的区别,也是一个优化(区别基本上都是优化):
这里遍历了2次,hashmap也就遍历了1次,其实遍历1次就够了,遍历链表,分成2个链表,1个改变下标的链表,1个不改变的。为啥这里遍历2次呢?
第一次for:

 //遍历链表
for (Node<K,V> p = f.next; p != null; p = p.next) {
    
    
    //计算值
    int b = p.hash & n;
    //判断是否不一致,不一致说明整个链表不能直接挪到新数组,需要拆分挪到2个不一样的下标
    if (b != runBit) {
    
    
        runBit = b;
        //此时的lastRun是另一个链表的头节点
        lastRun = p;
    }
}

第1个for,其实为了lastRun
如果此下标的值都不需要改变下标,那么lastRun = ff是头节点。
那第2个for的判断p != lastRun就不满足,就不需要走第2个for了。
如果此下标的值有需要改变下标的,那么lastRun就是另一个链表的头节点

打个比方:比如此时的数组长度是64
有1,65,193,321,129,257,此时准备扩容,那么走完第1个for,此时的lastRun = 129 ,并且后面挂着257,也就是所有在129后面的不需要改变下标的节点。
有1,129,257,65,193,321,此时准备扩容,那么走完第1个for,此时的lastRun = 65 ,并且后面挂着193,321,也就是所有在65后面的不需要改变下标的节点。
第2个for:

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有值,那么这个链表就是一个正反一半的链表,hn就是一个反向链表
        ln = new Node<K,V>(ph, pk, pv, ln);
    else
        //同样
        hn = new Node<K,V>(ph, pk, pv, hn);
}

注意此时的p != lastRun满足,就会走这个for。
假设是1,65,193,321,129,257,那么lastRun = 129
走完之后ln是1,129,257,hn是321,193,65
假设是1,129,65,193,321,257,那么lastRun = 257
走完之后ln是129,1,257,hn是321,193,65
一个是正反一半的链表,一个是反向链表。
仔细看这个for,从头开始遍历,每次指向前面的节点,应该全是反向节点,为啥会出现正反一半呢,因为p != lastRun
遍历到lastRun的时候,因为lastRun后面跟着原来在他后面的节点(跟lastRun下标一样的),就不需要遍历了。
这样就少new了一些node,细想一下,真的超级牛叉,concurrentHashMap真的精华非常多,很多细小的点,都是极致性能优化。

`扩容结束的善后

            //i < 0:只会是-1,表示当前线程是最后一份任务,并且完成了
            //i >= n,i + n >= nextn说实话,这2个判断真没看明白,有大佬看到这里,麻烦指点一下
            if (i < 0 || i >= n || i + n >= nextn) {
    
    
                int sc;
                //判断扩容是否完成
                if (finishing) {
    
    
                    nextTable = null;
                    table = nextTab;
                      //更新sizeCtl,也就是阈值,1.5倍(用的位运算,最大程度的提高性能)
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }
                //如果 sc == 标识符 + 1 (扩容结束了,不再有线程进行扩容)
                //默认第一个线程设置sc的低16 +2,一个线程结束了,就会将 sc -1。最后就是1也就是rs +1
                //把SIZECTL-1,表示扩容线程-1,注意sc = sizeCtl,是没-1之前的
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
    
    
                      //判断是不是最后一个扩容线程
                      //因为第一个线程扩容是SIZECTL+2,后面再来是SIZECTL+1,每个扩容线程退出时,SIZECTL-1,到最后一个(-1之前),应该是SIZECTL+2
                      //而(sc - 2)也就是(sizeCtl - 2),sizeCtl的低16位代表了扩容线程,如果最后一个那就是2,-2就是0
                      //resizeStamp(n) << RESIZE_STAMP_SHIFT的低16位全是0,要是不相等说明不是最后一个
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    finishing = advance = true;
                      //到这说明是最后一个扩容线程,把finishing改为true,把i改成n
                      //i改成n              是为了再检查一遍,是否全部扩容完毕,--i >= bound每次会满足,然后再走下面的else if去一一校验
                      //finishing改为true   是为了上面的一步全部检查之后,就可以真正退出了
                    i = n; // recheck before commit
                }
            }

这里其实还是扩容之前的判断,记住扩容的时候sizeCtl高16表示数组的长度,最高位是1低16表示扩容的线程数+1,因为第一个线程扩容是+2,后面是每多一个线程就是+1。
想通了这个,这些判断就很好理解了,Doug Lea牛逼。
长舒一口气,扩容终于结束了。
在这里插入图片描述

helpTransfer():

好了,大家冷静一下,回到put的第二部分的helpTransfer:

    final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
    
    
        Node<K,V>[] nextTab; int sc;
        if (tab != null && (f instanceof ForwardingNode) &&
            (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
    
    
            int rs = resizeStamp(tab.length);
            while (nextTab == nextTable && table == tab &&
                   (sc = sizeCtl) < 0) {
    
    
                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;
    }

哈哈,没有一句注释。其实这里跟准备扩容的判断一模一样,大家可以自己加注释,复习一遍。

好了,整个put全结束了,精华太多了,本次就分享到这,还有一些细节,大家可以去看我的下篇文章:细节篇,下篇分析ThreadPoolExecutor的,或者大家也可以留言分析啥。

猜你喜欢

转载自blog.csdn.net/couguolede/article/details/108530720
今日推荐