通俗易懂地ConcurrentHashMap解析 基于JDK1.8

看过Java并发编程艺术的绝对对ConcurrentHashMap不会感到陌生;但是由于书籍的出版,尤其经典书籍的出版都经历了漫长的岁月。。。因而很多东西都无法跟上时代(互联网行业)的快速发展:比如Hashtable(看到一个说法,Hashtbale被淘汰上因为没有遵循驼峰法)

通俗易懂地ConcurrentHashMap解析 基于JDK1.8_第1张图片

吐槽结束。现在回到CMAP。本文乃作者心血,自己认真研读了源码一周,并结合源码参考了世面上对ConcurrentHashMap优秀的文章解读,即使文章写得不行,拉到底阅读参考文献一定让你对ConcurrentHashMap有一个深入的理解(配合源码实用最佳);写到put方法,回来先提醒一下看官,本文,不涉及红黑树分析。学习红黑树请点赞出门右转;

经历了JDK1.8,CMAP可谓鸟枪换炮。仅从代码量上就能看到,从1600行膨胀到6300行;整体逻辑大变换;1.7的底层实现采用的上Segments分段锁机制,,可以理解为两层结构,具体不再分析,可以参考《Java并发编程的艺术》;

通俗易懂地ConcurrentHashMap解析 基于JDK1.8_第2张图片

而到了JDK1.8版本,Doug Lea 又将ConcurrentHashMap改回了一层结构,不再使用分段锁,改为使用CAS+synchronized的结构,近一步细分的锁的粒度

我们知道,1.7版本的CMAP锁的数量与并发等级concurrencyLevel有关,初始化后就确定了;而1.8版本后,锁的数量即为桶的数量;随着你的扩容会导致桶的数量翻倍,扩容后支持的最大并发访问数也同时翻倍;

同时,1.8版本的HashMap都引进了红黑树,当发生严重的Hash冲突时(同一个桶下的链表长度超过8个),此时会自动将链表结构转化为红黑树,从而时间复杂度从O(N)降低到了O(logN),而当桶下的结点数少于6个时,又会将红黑树转为链表结构;

那既然红黑树这么好,为什么用链表(单押*2):这是因为采用红黑树,需要左旋右旋,增加数据的时间损耗较大;而链表的增加删除都非常便捷;因此选用了8和6作为临界;既兼顾性能,又避免频繁的结构转换;

JDK1.8版本的 ConcurrentHashMap有几个非常重要的函数:putVal(),addCount(),transfer()等;下面一一开始介绍:

目录

构造方法

Put方法

initTable

addCount

helpTransfer

transfer

get

remove


构造方法

//创建一个空表,默认初始大小为16
public ConcurrentHashMap() 
//创建一个空表,指定了默认大小
public ConcurrentHashMap(int initialCapacity)  
//创建了一个表,并将复制参数中的所有表元素
public ConcurrentHashMap(Map m)
//创建一个表,指定了初始大小以及加载因子
public ConcurrentHashMap(int initialCapacity, float loadFactor) 
//创建一个表,指定了初始大小,加载因子和并发等级(默认为1);
public ConcurrentHashMap(int initialCapacity,
                         float loadFactor, int concurrencyLevel) 

首先是构造方法,不指定的话则为创建一个空表,经过初始化后默认大小为16,加载因子为0.75,并发等级为1;

为什么说经过初始化后默认大小为16呢,因为无参的构造方法是一个空实现,当我们put元素之后才会进行初始化工作,构造一个表;

Put方法

put方法非常重要。需要彻底理解,下面根据源码及注释一步步理解;

    public V put(K key, V value) {
        return putVal(key, value, false);
    }

    /** Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
    //不允许NULL的键和值,因为并发情况下无法分辨是不存在Key还是没有找到,
    //以及Value本身就为空,所以不允许NULL的存在;HashMap可以判断,因为containsKey
    //方法存在。而在多线程中,contains后去get,可能会发生修改或者删除,无法判断;
        if (key == null || value == null) throw new NullPointerException();
       //计算Hash值
         int hash = spread(key.hashCode());
        int binCount = 0;
        //死循环, 可以CAS不断竞争,或者协助扩容后出来继续干活等等;
        for (Node[] tab = table;;) {
            Node f; int n, i, fh;
            //如果未进行初始化,则初始化;接下来分析;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            //如果目标桶位为空,则通过CAS插入;
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            //当第一个元素的Hash值为MOVED(-1),代表为ForWardingNode,正在扩容
            else if ((fh = f.hash) == MOVED)
            //则该线程协助扩容;
                tab = helpTransfer(tab, f);
            else {
            //无事发生,我们继续synchronized加锁后慢慢进行传统添加Node
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        //fh>=0,代表我是链表
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node e = f;; ++binCount) {
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        //红黑树,本文不讲
                        else if (f instanceof TreeBin) {
                            Node p;
                            binCount = 2;
                            if ((p = ((TreeBin)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

总体来说,put方法还是比较易于理解的。下面简单的梳理一下流程

  • 根据 key 计算出 hashcode 。
  • 判断表是否需要进行初始化。
  •  定位出的目标桶,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋。
  • 如果当前位置的 hashcode == MOVED == -1,则帮助扩容,扩容完成后才进行插入。
  • 如果没有特殊情况,则利用 synchronized 锁写入数据。
  • 如果数量大于 TREEIFY_THRESHOLD(8) 则要转换为红黑树。
  • 插入完成,我们判断一下是不是要扩容,并记录一下Node数量;方便统计;

经过总结,是不是put方法就很简单明了了呢。 

当然,接下来我们就要深入探究,这些操作的具体原理,为什么能够帮助扩容?

But,我们还是先看看初始化吧

initTable

    /**
     * Initializes table, using the size recorded in sizeCtl.
     */
    private final Node[] initTable() {
        Node[] tab; int sc;
        //当前表为空,我们尝试当老大,把表初始化(单押)
        while ((tab = table) == null || tab.length == 0) {
            //小于0,我们在竞争中失败了,睡一觉等着别人初始化,初始化时sizeCtl为-1;
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // lost initialization race; just spin
            //好像还没人过来,我们赶紧通过CAS将sizeCtl置-1,慢慢初始化
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                //老子是第一个,开始初始化,完成后将sizeCtl设置为扩容阈值;
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node[] nt = (Node[])new Node[n];
                        table = tab = nt;
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

这个方法可真简单,就是多线程如果都想初始化,那就CAS竞争,谁将sizeCtl改为-1,就慢慢初始化,并将sizeCtl改为阈值(以待扩容)。失败的都休息,等扩容完成;

初始化完成,那我们接下来按照put流程,介绍一下addCount方法

addCount

addCount主要等作用有两个:

1、检测是否需要扩容;2、更新结点数量;

//添加计数,如果表太小而且尚未调整大小,则启动扩容。 如果已经调整大小,则在工作可用时帮助
//执行扩容。 在扩容后重新检查占用情况,看是否需要继续扩容。
//从 putVal 传入的参数是 1, binCount,binCount 是链表的长度/红黑树的结点数
    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[] tab, nt; int n, sc;
            //表的长度大于sizeCtl(阈值),且表不为空,表的长度小于最大值,则开始扩容
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                   (n = tab.length) < MAXIMUM_CAPACITY) {
                int rs = resizeStamp(n);
                //正在扩容
                if (sc < 0) {
                    // sizeCtl 变化了
                    //扩容结束;第一个线程设置 sc ==rs 左移 16 位 + 2,当线程结束扩容了,就会将 sc 减一。这个时候,sc 就等于 rs + 1
                    //达到最大扩容线程数
                    //扩容结束,则nextTable为空
                    //任务已被全部分配
                    //这么多情况下,我不需要帮助扩容
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;
                    //如果可以帮助扩容,那么将 sc 加 1. 表示多了一个线程在帮助扩容
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                }
                //没有在扩容,由我开启扩容状态,标识符左移 16 位 + 2. 也就是变成一个负数。高 16 位是标识符,低 16 位初始是 2.
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
                //统计元素数量
                s = sumCount();
            }
        }
    }

 

helpTransfer

put,remove等情况下遇到扩容,如果当前当前线程遇到Forwarding结点,发现正在扩容,就会帮助扩容;如果没有发现扩容,那么仍然可以继续操作;

    final Node[] helpTransfer(Node[] tab, Node f) {
        Node[] nextTab; int sc;
        if (tab != null && (f instanceof ForwardingNode) &&
            (nextTab = ((ForwardingNode)f).nextTable) != null) {
            int rs = resizeStamp(tab.length);
            while (nextTab == nextTable && table == tab &&
                   (sc = sizeCtl) < 0) {
                //类似addCount 不再赘述;
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || transferIndex <= 0)
                    break;
                //通过CAS操作获取扩容名额
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                    transfer(tab, nextTab);
                    break;
                }
            }
            return nextTab;
        }
        return table;
    }

讲解扩容前,必须先介绍一下sizeCtl属性,他有多种可能出现的值:

  • 0代表hash表还没有被初始化
  • 负数代表正在进行初始化或扩容操作
  • -1代表正在初始化
  • -(N+1) 表示有N个线程正在进行扩容操作
  • 正数数值表示下一次进行扩容的阈值,是当前ConcurrentHashMap容量的0.75倍,这与loadfactor是对应的;

transfer

扩容方法,非常重要

      
 private final void transfer(Node[] tab, Node[] nextTab) {
        int n = tab.length, stride;
        //算每条线程处理的桶个数,每条线程处理的桶数量一样;
        //如果CPU为单核,则使用一条线程处理所有桶;毕竟可能出现帮助扩容,大家不能越界
        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[] nt = (Node[])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,他的Hash值为-1(MOVED)
        //作用是告诉大家这个表正在扩容,快来帮忙;
        //以及,查询的时候看到我,指向了下一个表,你去那里看看;
        ForwardingNode fwd = new ForwardingNode(nextTab);
        boolean advance = true;
        boolean finishing = false; // to ensure sweep before committing nextTab
        //循环处理一个stride长度的任务,i后面会被赋值为该 stride 内最大的下标,而             
        //bound 后面会被赋值为该 stride 内左边界;通过循环不断减小i的值,从右往
        //左依次迁移桶上面的数据,直到i小于bound时结束该次长度为 stride 的迁移任务
        //结束这次的任务后会通过外层 addCount、helpTransfer、tryPresize 方法的
        // while 循环达到继续领取其他任务或者没有未分配的任务区间就休息;
        for (int i = 0, bound = 0;;) {
            Node f; int fh;
            while (advance) {
                int nextIndex, nextBound;
               //处理一个桶就i减1,进行
                if (--i >= bound || finishing)
                    advance = false;
                //transferIndex<=0证明任务分配完毕,i置-1,advance为false,后续根据这个退出扩容
                else if ((nextIndex = transferIndex) <= 0) {
                    i = -1;
                    advance = false;
                }
                //首次进入for循环会进入该函数,设置任务区间
                else if (U.compareAndSwapInt
                         (this, TRANSFERINDEX, nextIndex,
                          nextBound = (nextIndex > stride ?
                                       nextIndex - stride : 0))) {
                    bound = nextBound;
                    i = nextIndex - 1;
                    advance = false;
                }
            }
            //扩容结束,nextTable只有扩容时才不为null;将table指向新表,重新设置sizeCtl
            if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                if (finishing) {
                    nextTable = null;
                    table = nextTab;
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }
                //每当一条线程扩容结束就会更新一次 sizeCtl 的值,进行减1操作,扩容中,sizeCtl表示有多少个线程
                //正在扩容;
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                    //不是最晚一个干完活的,不用关灯
                    // 第一个扩容时候设置了U.compareAndSwapInt(this, SIZECTL, sc, 
                    //(rs << RESIZE_STAMP_SHIFT) + 2)
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    //最晚离开的,将i设置为n,再重新检查是不是所有的结点都完成转移了
                    finishing = advance = true;
                    i = n; // recheck before commit
                }
            }
            //空桶,放fwd标识扩容状态;
            else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);
            //已经放置了fwd,扩容了,检查下一个
            else if ((fh = f.hash) == MOVED)
                advance = true; // already processed
            // 加锁进行迁移;
            else {
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        Node ln, hn;
                        if (fh >= 0) {
                            int runBit = fh & n;
                            Node lastRun = f;
                            //解释一下lastRun,就是最后的连续N个相同的Node,我们需要将当前桶的元素根据
                            //前一位Hash值分到第i个桶和第i+n个桶上;那么lastRun代表的就是,最后连续的
                            //多个相同目标桶的的Node链表的第一个Node        
                            for (Node p = f.next; p != null; p = p.next) {
                                int b = p.hash & n;
                                if (b != runBit) {
                                    runBit = b;
                                    lastRun = p;
                                }
                            }
                            //根据runBit,确定是放到第i个桶还是第i+n个;
                            //LastRun结点后直接迁移,是修改指针,lastRun作为头结点
                            if (runBit == 0) {
                                ln = lastRun;
                                hn = null;
                            }
                            else {
                                hn = lastRun;
                                ln = null;
                            }
                            //除了LastRun结点,其他结点采用复制,倒序插入
                            //(倒序的原因是后插入的结点被访问的可能性更大)
                            for (Node p = f; p != lastRun; p = p.next) {
                                int ph = p.hash; K pk = p.key; V pv = p.val;
                                if ((ph & n) == 0)
                                    //next指向原来的ln,并ln引用指向自己,实现倒序;
                                    ln = new Node(ph, pk, pv, ln);
                                else
                                    hn = new Node(ph, pk, pv, hn);
                            }
                            //setTabAt方法调用的是 Unsafe 类的 putObjectVolatile 方法,将ln和hn挂到nextTable上;
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            //给原table设置fwd,标识迁移完成;
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                        //同上应该差不多,立个flag,等我学会了红黑树我就回来写
                        else if (f instanceof TreeBin) {
                            TreeBin t = (TreeBin)f;
                            TreeNode lo = null, loTail = null;
                            TreeNode hi = null, hiTail = null;
                            int lc = 0, hc = 0;
                            for (Node e = t.first; e != null; e = e.next) {
                                int h = e.hash;
                                TreeNode p = new TreeNode
                                    (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(lo) : t;
                            hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                                (lc != 0) ? new TreeBin(hi) : t;
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                    }
                }
            }
        }
    }

盗用参考文献3的一张图帮助大家理解LastRun:

通俗易懂地ConcurrentHashMap解析 基于JDK1.8_第3张图片

扩容时注意一点,根据treeifyBin()方法:

如果Table长度小于64而某个桶内结点超过8个,此时不会进行红黑树转化,而是直接进行扩容(即使表内结点总数没有超过sizeCtl),直到结点数量少于8个或者Table长度大于64(如果仍大于8,则转为树,不再继续扩容)

 

get

    //读无须加锁;不帮助扩容
    public V get(Object key) {
        Node[] tab; Node e, p; int n, eh; K ek;
        //获取hash
        int h = spread(key.hashCode());
        //找到了桶且不为空
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
           //链表查找
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            //树查找,hash为-2,如果正在扩容,fwd结点,hash为-1;
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }

一个重点:读不需要加锁;同时,结合上述的扩容操作。我们知道扩容从后往前进行。如果目标桶已经扩容完成,桶位会设置一个fwd,指向新的table;这时候可以通过fwd去新表上查找;如果还未扩容到当前桶,我们继续查找不受影响;这一个思想可以同时扩容和查找;如果正在扩容,LastRun采用直接转移,LastRun之前的采用复制,原table不受影响。因此也可以同时查找;

remove

    public V remove(Object key) {
        return replaceNode(key, null, null);
    }

    /**
     * Implementation for the four public remove/replace methods:
     * Replaces node value with v, conditional upon match of cv if
     * non-null.  If resulting value is null, delete.
     */
    //当value为空,则删除,否则根据cv进行替换
    final V replaceNode(Object key, V value, Object cv) {
        int hash = spread(key.hashCode());
        for (Node[] tab = table;;) {
            Node f; int n, i, fh;
            //表不存在 ,或者对应桶为空
            if (tab == null || (n = tab.length) == 0 ||
                (f = tabAt(tab, i = (n - 1) & hash)) == null)
                break;
           //先帮助扩容
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            //加锁 删除
            else {
                V oldVal = null;
                //判断是否被找
                boolean validated = false;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        //链表
                        if (fh >= 0) {
                            validated = true;
                            for (Node e = f, pred = null;;) {
                                K ek;
                                //找到目标结点
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    V ev = e.val;
                                    //cv为空,或者cv等于e.val,满足匹配条件
                                    if (cv == null || cv == ev ||
                                        (ev != null && cv.equals(ev))) {
                                        oldVal = ev;
                                        //value不为空,则替换
                                        if (value != null)
                                            e.val = value;
                                        //value为空,删除,跳过当前结点
                                        else if (pred != null)
                                            pred.next = e.next;
                                        else
                                            //目标位桶的头结点,直接将桶指向下一元素结点
                                            setTabAt(tab, i, e.next);
                                    }
                                    break;
                                }
                                pred = e;
                                if ((e = e.next) == null)
                                    break;
                            }
                        }
                        //红黑树
                        else if (f instanceof TreeBin) {
                            validated = true;
                            TreeBin t = (TreeBin)f;
                            TreeNode r, p;
                            if ((r = t.root) != null &&
                                (p = r.findTreeNode(hash, key, null)) != null) {
                                V pv = p.val;
                                if (cv == null || cv == pv ||
                                    (pv != null && cv.equals(pv))) {
                                    oldVal = pv;
                                    if (value != null)
                                        p.val = value;
                                    else if (t.removeTreeNode(p))
                                        setTabAt(tab, i, untreeify(t.first));
                                }
                            }
                        }
                    }
                }
                //找到了对应的值,则删除,并addCount记录元素数量变化
                if (validated) {
                    if (oldVal != null) {
                        if (value == null)
                            addCount(-1L, -1);
                        return oldVal;
                    }
                    break;
                }
            }
        }
        return null;
    }

remove和repace都是基于replaceNode(Object key, V value, Object cv) 执行的;所以在其中进行了判断,替换可以选择旧值满足条件才进行替换;

总结

  • 扩容后倒序,LastRun及后续结点除外

  • LastRun结点迁移采用修改引用方法,其他结点重新创建一个结点(深拷贝)

  • 扩容时,对桶对迁移也是从后往前进行迁移;未迁移到的桶可以正常getput等操作;迁移时原表不受 影响,可以正常get,put,remove需要锁,不能同时进行;

  • SizeCtl很重要,根据不同情况下可以作不同标识,ForwardingNode表示已经迁移完毕,并为get指向新表,为put,remove作扩容标识协助扩容;

  • 扩容完成后,最后一个线程需要重新检查是否有遗漏的桶未进行扩容;在扩容的时候每个线程都有处理的步长,和CPU核心有关,最少为16,在这个步长范围内的数组节点只有自己一个线程来处理

  • 引入红黑树后,HashMap均采用尾插法

参考文献(排名不分先后):

1、并发编程——ConcurrentHashMap#addCount() 分析:https://www.jianshu.com/p/749d1b8db066

2、并发容器之ConcurrentHashMap(JDK 1.8版本):https://juejin.im/post/5aeeaba8f265da0b9d781d16

3、ConcurrentHashMap1.8 - 扩容详解:https://blog.csdn.net/ZOKEKAI/article/details/90051567

4、ConcurrentHashMap源码分析(1.8):https://www.cnblogs.com/zerotomax/p/8687425.html

如果您对本文有质疑,希望能够指出,欢迎友好探讨~

你可能感兴趣的:(Java)