0202源码详解-ConcurrentHashMap-线程安全集合类-并发编程(Java)

4 transfer()-扩容

在size大小大于阈值(容量的3/4)时,会进行扩容;当某个操作检测到结点hash值为MOVED时,会帮忙扩容,间接调用 transfer();tryPresize()也会调用();源代码如下:

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range
    if (nextTab == null) {            // initiating
        try {
            @SuppressWarnings("unchecked")
            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 = 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 (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;
        while (advance) {
            int nextIndex, nextBound;
            if (--i >= bound || finishing)
                advance = false;
            else if ((nextIndex = transferIndex) <= 0) {
                i = -1;
                advance = false;
            }
            else if (U.compareAndSwapInt
                     (this, TRANSFERINDEX, nextIndex,
                      nextBound = (nextIndex > stride ?
                                   nextIndex - stride : 0))) {
                bound = nextBound;
                i = nextIndex - 1;
                advance = false;
            }
        }
        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
            }
        }
        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;
                        }
                        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;
                    }
                }
            }
        }
    }
}

多线程完成数据迁移主要模型如下图所示:在这里插入图片描述

  • 多线程迁移数据,为提高效率,采用分段的形式,来完成。
  • 数据迁移由后向前执行
  • 如果多个线程在同一范围内相遇,产生冲突,通过syncronized来实现并发访问控制控制
  • 数据迁移,重新hash计算等于0分到新nextTab的索引i处;不等于0分到i+n处;
  • 红黑树拆分的元素个数如果小于等于阈值6,那么红黑树就重新转换为单链表。

详细执行流程步骤如下:

  • 计算tab程度n,计算范围长度stride

  • 判断如果传参nextTab为null,说明是首个调整大小的线程

    • 初始化新的hash数组,大小为原数组大小的2倍
    • transferIndex置为n,该变量表示整个hash数组下一个要迁移范围的起始索引+1
  • 以nextTab新建ForwardingNode结点fwd

  • 设置advance用于确定线程要迁移的索引值;finishing用于确认是否迁移完毕

  • for循环,i初始0,bound(边界)初始0

    • while判断advance是否为true

      • nextIndex表示当前线程要下一个迁移数据范围的起始索引;nextBound表示当前线程下一个要迁移数据范围的结束边界;

      • 判断–i是否大于等于bound或者finishing已经结束

        • advance置为false
      • 否则判断(nextIndex=transferIndex)是否小于等于0

        • i置为-1;advance置为false
      • 否则cas设置transferIndex值由nextIndex变为新计算nextBound

        • 当前要迁移范围边界设置为nextBound
        • i置为nextIndex-1
        • advance置为false
    • 确定迁移范围起始索引i后,判断如果i小于0或者i大于等于n或者i+n大于等于nextn新数组长度即2n

      • 表示当前线程迁移已经执行到数组头,有其他线程迁移工作还未完成或者全部完成。

      • 判断是否finishing

        • sizeCtl置为新容量的3/4,返回
      • 判断cas把sizeCtl-1是否成功

        • 判断sc-2是否等于最初设置rs(戳左移16位),即判断是否不是最后一个迁移线程
          • 不是最后一个迁移线程,直接return
        • 是最后一个迁移线程,finishing,advanc置为true,i置为n
    • 否则判断索引i处的结点f是否为null

      • 直接cas把索引i又null置为fwd
    • 否则判断f的hash值fh是否等于MOVED

      • 表示此索引下数据已经迁移完成
      • advance置为true
    • 否则执行具体的迁移逻辑

      • 对syncronized对索引i下的首结点f加锁
        • 判断索引i处结点是否等于f
          • 判断fh是否大于等于0

            • 此索引下为单链表结构

            • 计算fh&n的值runBit

            • for循环链表,查找结点hash&n结构不一样的

              • 一般情况下该链表中hash都是相等的,计算结果也相等;不相等的情况另外讨论;
            • 判断runBit等于0

              • ln指向lastRun;hn置空
            • 否则

              • hn指向lastRun;ln置空
            • 处理链表结点hash不同的情况

            • setTabAt()吧nextTab索引i处置为ln

            • setTabAt()吧nextTab索引i+n处置为hn

            • setTabAt()吧tab索引i处置为fwd

          • 否则判断f是否为TreeBin结点

            • 执行红黑树的迁移

            • for循环遍历first指向的双向链表

              • 以当前结点e的hash,key,value构建新的结点p
              • 判断如果结点hash值h&n等于0
                • p链接到双向链表lo中
                • lc计数+1
              • 否则p链接到双向链表hi,hc计数+1
            • 判断如果lc计数小于阈值6,ln指向lo转换后的单链表;否则判断hc是否不等于0

              • 不等于指向lo转换后的红黑树
              • 等于0指向原索引i下结点f
            • hn做相同的逻辑

            • setTabAt()吧nextTab索引i处置为ln

            • setTabAt()吧nextTab索引i+n处置为hn

            • setTabAt()吧tab索引i处置为fwd

问题:

  1. 源代码2433和2468行-hash数组大小调整,数据迁移的时候为什么要根据hash&n的结果来决定迁移到新hash数组中的索引位置?为什么运算结果为0迁移到索引i处而非0迁移到i+n处?
  2. 源代码2417行-在上面数据迁移判断i < 0 || i >= n || i + n >= nextn为true且cas确定最后一个迁移线程后,为什么没有直接收尾,结束程序而是又执行了一遍for循环呢
  3. 源代码2427行-在执行具体迁移逻辑加锁后,为什么还要通过tabAt()判断索引i出的结点是否等于f呢?f不是通过tabAt()获取的索引i处的结点吗?
  4. 2432行-什么情况下某个索引下结点的hash值会不同呢?

5 addCount()-增加计数

源码如下:

private final void addCount(long x, int check) {
    CounterCell[] as; long b, s;
    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(x, uncontended);
            return;
        }
        if (check <= 1)
            return;
        s = sumCount();
    }
    if (check >= 0) {
        Node<K,V>[] tab, nt; int n, sc;
        while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
               (n = tab.length) < MAXIMUM_CAPACITY) {
            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;
                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);
            s = sumCount();
        }
    }
}

执行流程如下:

  • 判断计数单元数组as不等于null或者cas设置baseCount为+x失败
    • 判断as为空或者as的长度小于1或者当前线程计算对应的as索引值a为空或者cas设置a为+x失败
      • 执行fullAddCount()方法,return
    • 如果check小于等于1,return
    • 执行sumCount()赋值s
  • 判断如果check大于等于0
    • 以下为执行扩容的判断与执行逻辑,不在详述

问题:

  1. 2275行-此处扩容逻辑与helpTransfer()有何不同?

6 fullAddCount()

之前LongAdder()有这部分知识,这里只做简要分析:

  • 如果计数单元数组不为空
    • 当前线程对应的数组索引值为空,创建CounterCell
    • 不为空cas+x
    • 多次cas失败,执行数组扩容
  • 否则判断计数单元数组为空,初始化
  • 否则cas设置baseCount

问题:

  1. 某个线程在执行扩容counterCells之前,经历了几次cas失败?

7 size()计算

public int size() {
    long n = sumCount();
    return ((n < 0L) ? 0 :
            (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
            (int)n);
}

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;
}

size计算发生在put,remove等改变集合的操作中,多线程访问的情况下,sumCount()计数并不完全准确。

8 remove() 删除

删除执行流程可参考put(),问题:

  1. 1156行-为什么在移除红黑树结点返回true的时候会直接cas设置为untreeify(t.first)后的单链表结构?

后记

如有问题,欢迎交流讨论。

❓QQ:806797785

⭐️源代码仓库地址:https://gitee.com/gaogzhen/concurrent

参考:

[1]黑马程序员.黑马程序员深入学习Java并发编程,JUC并发编程全套教程[CP/OL].2020-01-18/2022-12-12.p281~p289.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

gaog2zh

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值