JUC系列并发容器之ConcurrentHashMap(JDK1.8版)扩容图解说明

Posted 顧棟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JUC系列并发容器之ConcurrentHashMap(JDK1.8版)扩容图解说明相关的知识,希望对你有一定的参考价值。

ConcurrentHashMap(JDK1.8)扩容说明

文章目录

扩容过程

触发条件

第一种场景在新增结点的时候,在addCount中检查元素个数时,若到达扩容阈值时,若数组长度小于64会调用tryPresize对Node数组进行2倍扩容transfer方法对结点的位置进行重分配。

第二种场景是在新增结点的时候,在当桶中链表结点数超过8个,会调用treeifyBin尝试进行将链表转为红黑树,但是若数组小于64,则也会对Node数组进行扩容,不链表转为红黑树。

基本原理

新建一个Node数组,其长度为旧数组长度的2倍,然后将旧的元素迁移到新数组中。

可以有一个或多个线程一起进行数据迁移,单核不支持多线程并行迁移。

迁移的过程中,旧数组的长度是N,每个线程扩容一段,一段的长度使用遍历stride(步长)来表示,为了降低资源竞争频率,stride最小的范围长度是16。transferIndex表示整个数组扩容的进度,每个调用tranfer的线程会对当前旧table中[transferIndex-stride, transferIndex-1]位置的结点进行迁移,不过是倒叙的方式进行的。

步骤

具体实现主要集中在transfer(Node<K,V>[] tab, Node<K,V>[] nextTab)方法中。

  1. 计算出stride的值 MIN_TRANSFER_STRIDE是16。

    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE;
    
  2. 首次扩容,进行新Node数组的初始化。

        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 指向最右边的桶,也就是数组索引下标最大的位置
            transferIndex = n;
        
    
  3. 由advance变量控制桶的遍历,是否进行下一个桶的迁移;由finishing变量控制数组整个扩容是否结束。

  4. for循环中进行不通过条件处理,while结构体中,通过CAS操作TRANSFERINDEX,成功执行CAS,说明获取这个桶的结点迁移执行权,多个线程之间也是通过TRANSFERINDEX并行进行不同桶的迁移,在对应的桶的迁移时,advance为false。

    处理的逻辑分为4种:

    case1:当前的任务是整个数组扩容的最后一个任务或者扩容冲突。

            if (i < 0 || i >= n || i + n >= nextn) 
                int sc;
                //扩容结束后做后续工作,将 nextTable 设置为 null,表示扩容已结束,将 table 指向新数组,sizeCtl 设置为扩容阈值
                if (finishing) 
                    nextTable = null;
                    table = nextTab;
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                
                //每当一条线程扩容结束就会更新一次 sizeCtl 的值,进行减 1 操作
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) 
                    //若成立,说明该线程不是扩容大军里面的最后一条线程,直接return回到上层while循环
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    //(sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT 说明这条线程是最后一条扩容线程
                    //之所以能用这个来判断是否是最后一条线程,因为第一条扩容线程进行了如下操作:
                    //    U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)
                    //除了修改结束标识之外,还得设置 i = n; 以便重新检查一遍数组,防止有遗漏未成功迁移的桶
                    finishing = advance = true;
                    i = n; // recheck before commit
                
            
    

    case2:该桶为空即Node数组i下标中为null。

            else if ((f = tabAt(tab, i)) == null)
                // 遇到数组上空的位置直接放置一个占位对象,以便查询操作的转发和标识当前处于扩容状态
                advance = casTabAt(tab, i, null, fwd);
    

    case3:该桶(Node数组i下标的元素)已经完成迁移。

        else if ((fh = f.hash) == MOVED)
            // 数组上遇到hash值为MOVED,也就是 -1 的位置,说明该位置已经被其他线程迁移过了,将 advance 设置为 true ,以便继续往下一个桶检查并进行迁移操作
            advance = true; // already processed
    

    case4:该桶还未迁移,根据桶首结点的hash值进行链表迁移或红黑树迁移。

    链表迁移:找到最后一个与相邻结点runbit不同的结点lastRun。通过这个lastRun将链表分为两个子链表ln和hn,将他们链接到新数组的ii+n的位置。

    红黑树迁移:同样将列表拆分为两个子链表,在插入新数组之前会判断子链表的结点是否满足红黑树个数要求,进行红黑树与链表转换。

            synchronized (f) 
                if (tabAt(tab, i) == f) 
                    Node<K,V> ln, hn;
                    //该桶下为链表结构
                    if (fh >= 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;
                        //遍历整条链表,找出 lastRun 节点
                        for (Node<K,V> p = f.next; p != null; p = p.next) 
                            int b = p.hash & n;
                            if (b != runBit) 
                                runBit = b;
                                lastRun = p;
                            
                        
                        //根据 lastRun 节点的高位标识(0 或 1),首先将 lastRun设置为 ln 或者 hn 链的末尾部分节点,后续的节点使用头插法拼接
                        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方法调用的是 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;
                    
                    //该桶下为红黑树结构
                    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;
                    
                
            
    

图解


以上是关于JUC系列并发容器之ConcurrentHashMap(JDK1.8版)扩容图解说明的主要内容,如果未能解决你的问题,请参考以下文章

JUC系列并发容器之ConcurrentLinkedQueue(JDK1.8版)

JUC系列并发容器之ConcurrentHashMap(JDK1.8版)

JUC系列并发容器之ConcurrentHashMap(JDK1.8版)扩容图解说明

Java并发编程系列之三JUC概述

JUC系列01之大话并发

Java并发编程系列之三JUC概述