ConcurrentHashMap源码分析(二)

简介

​        ConcurrenHashMap 在扩容过程中主要使用 sizeCtl 和 transferIndex 这两个属性来协调多线程之间的并发操作,并且在扩容过程中大部分数据依旧可以做到访问不阻塞 。

sizeCtl 属性在各个阶段的作用

(1)、新建而未初始化时

int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY : tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap;

用处:sizeCtl 用于记录初始容量大小,仅用于记录集合在实际创建时应该使用的大小的作用 。

(2)、初始化过程中

U.compareAndSwapInt(this, SIZECTL, sc, -1)

用处:将 sizeCtl 值设置为 -1 表示集合正在初始化中,其他线程发现该值为 -1 时会让出CPU资源以便初始化操作尽快完成 。

(3)、初始化完成后

Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
sizeCtl = sc;

用处:sizeCtl 用于记录当前集合的负载容量值,也就是阈值(最大长度*0.75)。

(4)、正在扩容时

//第一条扩容线程设置的某个特定基数
U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)
//后续线程加入扩容大军时每次加 1
U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)
//线程扩容完毕退出扩容操作时每次减 1
U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)

用处:sizeCtl 用于记录当前扩容并发线程数情况,此时 sizeCtl 的值为:((rs << RESIZE_STAMP_SHIFT) + 2) + (正在扩容的线程数) ,并且该状态下 sizeCtl < 0 。

什么时候会扩容?

当往hashMap中成功插入一个key/value节点时,有两种情况可能触发扩容动作:

1、如果新增节点之后,所在链表的元素个数达到了阈值 8,则会调用treeifyBin方法把链表转换成红黑树,不过在结构转换之前,会对数组长度进行判断,实现如下:如果数组长度n小于阈值MIN_TREEIFY_CAPACITY,默认是64,则会调用tryPresize方法把数组长度扩大到原来的两倍,并触发transfer方法,重新调整节点的位置。

2、调用put方法新增节点时,在结尾会调用addCount方法记录元素个数,并检查是否需要进行扩容,当数组元素个数达到阈值时,会触发transfer方法,重新调整节点的位置。

3、扩容状态下其他线程对集合进行插入、修改、删除、合并、compute 等操作时遇到 ForwardingNode 节点会触发扩容 。

4、putAll 批量插入或者插入节点后发现存在链表长度达到 8 个或以上,但数组长度为 64 以下时会触发扩容 。

注意:桶上链表长度达到 8 个或者以上,并且数组长度为 64 以下时只会触发扩容而不会将链表转为红黑树 。

transfer()扩容方法

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;  //stride 主要和CPU相关
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)  //每个线程处理桶的最小数目,可以看出核数越高步长越小,最小16个。
        stride = MIN_TRANSFER_STRIDE; // subdivide range
    if (nextTab == null) {
        try {
            @SuppressWarnings("unchecked")
            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];  //扩容到2倍
            nextTab = nt;
        } catch (Throwable ex) {      // try to cope with OOME
            sizeCtl = Integer.MAX_VALUE;  //扩容保护
            return;
        }
        nextTable = nextTab;
        transferIndex = n;  //扩容总进度,>=transferIndex的桶都已分配出去。
    }
    int nextn = nextTab.length;
      //扩容时的特殊节点,标明此节点正在进行迁移,扩容期间的元素查找要调用其find()方法在nextTable中查找元素。
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab); 
    //当前线程是否需要继续寻找下一个可处理的节点
    boolean advance = true;
    boolean finishing = false; //所有桶是否都已迁移完成。
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;
        //此循环的作用是确定当前线程要迁移的桶的范围或通过更新i的值确定当前范围内下一个要处理的节点。
        while (advance) {
            int nextIndex, nextBound;
            if (--i >= bound || finishing)  //每次循环都检查结束条件
                advance = false;
            //迁移总进度<=0,表示所有桶都已迁移完成。
            else if ((nextIndex = transferIndex) <= 0) {  
                i = -1;
                advance = false;
            }
            else if (U.compareAndSwapInt
                     (this, TRANSFERINDEX, nextIndex,
                      nextBound = (nextIndex > stride ?
                                   nextIndex - stride : 0))) {  //transferIndex减去已分配出去的桶。
                //确定当前线程每次分配的待迁移桶的范围为[bound, nextIndex)
                bound = nextBound;
                i = nextIndex - 1;
                advance = false;
            }
        }
        //当前线程自己的活已经做完或所有线程的活都已做完,第二与第三个条件应该是下面让"i = n"后,再次进入循环时要做的边界检查。
        if (i < 0 || i >= n || i + n >= nextn) {
            int sc;
            if (finishing) {  //所有线程已干完活,最后才走这里。
                nextTable = null;
                table = nextTab;  //替换新table
                sizeCtl = (n << 1) - (n >>> 1); //调sizeCtl为新容量0.75倍。
                return;
            }
            //当前线程已结束扩容,sizeCtl-1表示参与扩容线程数-1。
            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
              //还记得addCount()处给sizeCtl赋的初值吗?相等时说明没有线程在参与扩容了,置finishing=advance=true,为保险让i=n再检查一次。
                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);  //如果i处是ForwardingNode表示第i个桶已经有线程在负责迁移了。
        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) {  //>=0表示是链表结点
                        //由于n是2的幂次方(所有二进制位中只有一个1),如n=16(0001 0000),第4位为1,那么hash&n后的值第4位只能为0或1。所以可以根据hash&n的结果将所有结点分为两部分。
                        int runBit = fh & n;
                        Node<K,V> lastRun = f;
                        //找出最后一段完整的fh&n不变的链表,这样最后这一段链表就不用重新创建新结点了。
                        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;
                        }
                        //lastRun之前的结点因为fh&n不确定,所以全部需要重新迁移。
                        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);
                        }
                        //低位链表放在i处
                        setTabAt(nextTab, i, ln);
                        //高位链表放在i+n处
                        setTabAt(nextTab, i + n, hn);
                        setTabAt(tab, i, fwd);  //在原table中设置ForwardingNode节点以提示该桶扩容完成。
                        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;
                            }
                        }
                        //如果拆分后的树的节点数量已经少于6个就需要重新转化为链表
                        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;
                            //CAS存储在nextTable的i位置上
                        setTabAt(nextTab, i, ln);
                          //CAS存储在nextTable的i+n位置上
                        setTabAt(nextTab, i + n, hn);
                        //CAS在原table的i处设置forwordingNode节点,表示这个这个节点已经处理完毕
                        setTabAt(tab, i, fwd);
                        advance = true;
                    }
                }
            }
        }
    }
}

源码很长,我们分开解读,第一部分:

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;  //stride 主要和CPU相关
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)  //每个线程处理桶的最小数目,可以看出核数越高步长越小,最小步长16个。
            stride = MIN_TRANSFER_STRIDE; // subdivide range
        if (nextTab == null) {
            try {
                @SuppressWarnings("unchecked")
                Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];  //扩容到2倍
                nextTab = nt;
            } catch (Throwable ex) {      // try to cope with OOME
                sizeCtl = Integer.MAX_VALUE;  //扩容保护
                return;
            }
            nextTable = nextTab;
            transferIndex = n;  //扩容总进度,>=transferIndex的桶都已分配出去。
            int nextn = nextTab.length;
        }

 

  • n为扩容之前数组的长度,stride 主要和CPU相关,含义为步长,每个线程在扩容时拿到的长度,最小为16。

  • nextTab为新的table,如果nextTab为空就新建一个table,大小为原来的2倍,代表双倍扩容。

  • transferIndex=n;n为扩容前的大小。

  • int nextn = nextTab.length; nextn为扩容后数组的大小

//构造一个ForwardingNode用于多线程之间的共同扩容情况
 ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
​
 boolean advance = true; //继续遍历的确认标志
 boolean finishing = false; //所有桶是否都已迁移完成标志
  • fwd是一个标志,标明此节点正在进行迁移。当其他线程进行操作的时候发现这个位置存放的是fwd就知道正在进行扩容。、

  • advance是遍历的确认标志,是否再往前进行遍历。finishing 是所有桶是否都已迁移完成标志

//遍历每个节点
  for (int i = 0, bound = 0;;) {
      Node<K,V> f; int fh; //定义一个节点和一个节点状态判断标志fh
      while (advance) {
          int nextIndex, nextBound;
          if (--i >= bound || finishing) 每次循环都检查结束条件
              advance = false;
          //迁移总进度<=0,表示所有桶都已迁移完成
          else if ((nextIndex = transferIndex) <= 0) {
              i = -1;  
              advance = false;
          }
  • 接下来开始遍历每一个节点,bound是当前步长结尾的位置。初始化一个节点f和一个节点状态判断标志fh

  • while循环的作用是确定当前线程要迁移的桶的范围或通过更新i的值确定当前范围内下一个要处理的节点。

  • 当前步长内元素转移完成后 i = -1

else if (U.compareAndSwapInt
          (this, TRANSFERINDEX, nextIndex,
           nextBound = (nextIndex > stride ?
                        nextIndex - stride : 0))) {  //transferIndex减去已分配出去的桶。
     //确定当前线程每次分配的待迁移桶的范围为[bound, nextIndex)
     bound = nextBound;
     i = nextIndex - 1;
     advance = false;
 }

这里是一个CAS的计算来修改TRANSFERINDEX(转移到的下标),配合上面一段代码计算出当前线程操作数组的具体区域。

if (i < 0 || i >= n || i + n >= nextn) {
        int sc;
        //如果原table已经复制结束
        if (finishing) {
            nextTable = null; //可以看出在扩容的时候nextTable只是类似于一个temp用完会丢掉
            table = nextTab;
            sizeCtl = (n << 1) - (n >>> 1); //修改扩容后的阀值,应该是现在容量的0.75倍
            return;//结束循环
        }
        //采用CAS算法更新SizeCtl。
        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
        }
    }
  • i < 0 || i >= n || i + n >= nextn一共三个条件。1.当前线程自己的活已经做完或所有线程的活都已做完,第二与第三个条件应该是下面让"i = n"后,再次进入循环时要做的边界检查。

  • 如果当前线程的工作做完后发现此时已经finialing了,就可以把sizeCtl 改为新的值结束循环。

//CAS算法获取数组第i的节点,为空就设为forwordingNode
else if ((f = tabAt(tab, i)) == null)
   advance = casTabAt(tab, i, null, fwd);
//如果这个节点的hash值是MOVED,就表示这个节点是forwordingNode节点,就表示这个节点已经被处理过了,直接跳过
else if ((fh = f.hash) == MOVED)
   advance = true; // already processed

如注释所示,CAS算法获取数组第i个位置的值,为空就设为fwd标志位。如果这个节点的hash值是MOVED,代表内容已经是fwd,被其他线程处理过了,直接跳过,继续前进。

  • 这里先根据 fh 判断头的位置是链表的头节点还是树的根节点,如果是链表的话就执行链表转移,在转移过程中使用的是CAS算法。使用头插法进行转移。

  • 如果是红黑树的话转移的方法和HashMap1.8中对红黑树的转移是一样的,是使用了两个链表,一个是高位链表一个是低位链表:

else if (f instanceof TreeBin) {
      TreeBin<K,V> t = (TreeBin<K,V>)f;
      //lo 为低位链表头结点,loTail 为低位链表尾结点,hi 和 hiTail 为高位链表头尾结点
      TreeNode<K,V> lo = null, loTail = null;
      TreeNode<K,V> hi = null, hiTail = null;
      int lc = 0, hc = 0;
      //同样也是使用高位和低位两条链表进行迁移
      //使用for循环以链表方式遍历整棵红黑树,使用尾插法拼接 ln 和 hn 链表
      for (Node<K,V> e = t.first; e != null; e = e.next) {
          int h = e.hash;
          //这里面形成的是以 TreeNode 为节点的链表
          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;
          }
      }
      //形成中间链表后会先判断是否需要转换为红黑树:
      //1、如果符合条件则直接将 TreeNode 链表转为红黑树,再设置到新数组中去
      //2、如果不符合条件则将 TreeNode 转换为普通的 Node 节点,再将该普通链表设置到新数组中去
      //(hc != 0) ? new TreeBin<K,V>(lo) : t 这行代码的用意在于,如果原来的红黑树没有被拆分成两份,那么迁移后它依旧是红黑树,可以直接使用原来的 TreeBin 对象
      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方法调用的是 Unsafe 类的 putObjectVolatile 方法
      //使用 volatile 方式的 putObjectVolatile 方法,能够将数据直接更新回主内存,并使得其他线程工作内存的对应变量失效,达到各线程数据及时同步的效果
      //使用 volatile 的方式将 ln 链设置到新数组下标为 i 的位置上
      setTabAt(nextTab, i, ln);
​
      //使用 volatile 的方式将 hn 链设置到新数组下标为 i + n(n为原数组长度) 的位置上
      setTabAt(nextTab, i + n, hn);
​
      //迁移完成后使用 volatile 的方式将占位对象设置到该 hash 桶上,该占位对象的用途是标识该hash桶已被处理过,以及查询请求的转发作用
      setTabAt(tab, i, fwd);
​
      //advance 设置为 true 表示当前 hash 桶已处理完,可以继续处理下一个 hash 桶
      advance = true;
  }

helpTransfer()

这个方法是帮助其他线程进行扩容。添加、删除节点之处都会检测到table的第i个桶是ForwardingNode的话会调用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;
    }

tryPresize()

putAll批量插入或者插入节点后发现链表长度达到8个或以上,但数组长度为64以下时触发的扩容会调用到这个方法.

private final void tryPresize(int size) {
        //根据传入的size计算出真正的新容量,因为新容量需要是2的幂次方。
        int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
            tableSizeFor(size + (size >>> 1) + 1);
        int sc;
        //如果不满足条件,也就是 sizeCtl < 0 ,说明有其他线程正在扩容当中,这里也就不需要自己去扩容了,结束该方法
        while ((sc = sizeCtl) >= 0) {
            Node<K,V>[] tab = table; int n;
            //如果数组没有初始化则进行初始化,这个选项主要是为批量插入操作方法 putAll 提供的
            if (tab == null || (n = tab.length) == 0) {
                n = (sc > c) ? sc : c;   //table未初始化则给一个初始容量
                //初始化时将 sizeCtl 设置为 -1
                if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                    try {
                        if (table == tab) {
                            @SuppressWarnings("unchecked")
                            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                            table = nt;
                            sc = n - (n >>> 2);
                        }
                    } finally {
                      //初始化完成后 sizeCtl 用于记录当前集合的负载容量值,也就是触发集合扩容的阈值
                        sizeCtl = sc;
                    }
                }
            }
            else if (c <= sc || n >= MAXIMUM_CAPACITY)
                break;
            //插入节点后发现链表长度达到8个或以上,但数组长度为64以下时触发的扩容会进入到下面这个 else if 分支
            else if (tab == table) {
                int rs = resizeStamp(n);
                if (sc < 0) {
                    Node<K,V>[] nt;
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        //传入指定容量
                        transfer(tab, nt);
                }
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
            }
        }
    }

扩容过程图解

触发扩容的操作:

假设目前数组长度为8,数组的元素的个数为5。再放入一个元素就会触发扩容操作。

总结一下扩容条件:

(1) 元素个数达到扩容阈值。

(2) 调用 putAll 方法,但目前容量不足以存放所有元素时。

(3) 某条链表长度达到8,但数组长度却小于64时

CPU核数与迁移任务hash桶数量分配(步长)的关系

单线程下线程的任务分配与迁移操作

多线程如何分配任务?

普通链表如何迁移?

红黑树如何迁移?

hash桶迁移中以及迁移后如何处理存取请求?

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值