从源码探究 1.8 ConcurrentHashMap 的几个使用过程中想到的问题

前言

水平有限,尽量深入

主要关注的点

  • put 方法相关
    • put 方法做了哪些事
    • 如何保证并发 put 安全(cas 和 synchronized 的使用)
    • 扩容相关
      • 扩容过程
      • 扩容如何保证并发安全性
  • get 方法线程安全
  • size 机制

使用 ConcurrentHashMap 中的一些疑问解析

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) {
        // key 和 value 均不能为 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)
                // 通过 cas 操作,保证初始化的并发安全
                tab = initTable();
            // 如果要put 的 key 要放置的桶为空,则直接将new 的 node ,利用 cas 操作设置为头结点
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            // 如果当前 table 正在扩容,那么当前线程需要帮助进行扩容
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                // 锁住头结点,这样其他线程就无法操作这个桶上所有的 node
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                // 如果 key 已经存在,则覆盖value
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                // 否则进行尾插
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        // 如果头结点是树节点,则在红黑树中插入
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                // binCount >= 8,则代表当前的链表需要转换为红黑树
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        // count + 1,并扩容
        addCount(1L, binCount);
        return null;
    }

put方法流程:

  1. key value 的 null 处理
  2. 计算 hash
  3. 如果 table 没有初始化,则需要初始化
  4. 如果 key 对应的桶为空,则需要 cas 将新节点作为头结点放在在桶中
  5. 如果当前 table 正在扩容,那么当前线程需要帮忙扩容
  6. 如若没有进 3.4.5分支,则需要根据 hashcode 判断是 replace 或者是插入到链表/树中
  7. 如果是插入操作
    1. 如果是插入链表,则还需要判断是否转化成红黑树
    2. 插入之后,需要通过 addCount 进行 count 的加一,以及扩容操作

并发环境下,put操作如何保证线程安全:

  1. 如果 key 对应的桶中,还没有存放节点,那么使用 cas 操作,将首节点设置到桶中。多线程条件下,只有一个线程能通过 cas 设置首节点。
  2. 如果 key 对应的桶中,已经存放了一些节点,那么通过对首节点进行 synchronized 操作,保证同一个桶,同一个时间点,只有一个线程在操作。

扩容流程:
有两个操作会引发扩容:

  • 当桶中节点由链表结构转换为红黑树时,treeifyBin 操作
  • 当插入节点后,addCount 操作
  • 如果一个线程在扩容过程中,另外一个线程要插入数据,则需要帮助扩容,进行 helpTransfer 操作

这三个操作进行扩容,核心都是调用方法:

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab)

分析具体怎么扩容之前,需要了解ConcurrentHashMap 很重要的一个参数:sizeCtl
这个参数在初始化table、扩容的过程中都有涉及到:
(1)、sizeCtl 为 -1:初始化过程中
U.compareAndSwapInt(this, SIZECTL, sc, -1)
作用:将 sizeCtl 值设置为 -1 表示集合正在初始化中,其他线程发现该值为 -1 时会让出CPU资源以便初始化操作尽快完成 。
(2)、sizeCtl > 0:初始化完成后
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
sizeCtl = sc;
作用:sizeCtl 用于记录当前集合的负载容量值,也就是触发集合扩容的极限值 。
(3)、sizeCtl <0:正在扩容时
//第一条扩容线程设置的某个特定基数
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 & 0xFFFF)-1 来获取。
注意:这里有一些疑问,sizeCtl 参数的注释里写的是,当 sizeCtl < 0的时候,并发线程数是 n 的话,sizeCtl = -(n+1)。这里感觉有点问题,我自己看代码的时候,确实不是注释中写的那样子。第一条扩容线程设置的 siceCtl(计算方式为:U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2) ),其实并不是-2,而是很大的一个负数。这个原因在于扩容时,sizeCtl初始化的方式:(rs << RESIZE_STAMP_SHIFT) + 2,而 rs 的计算方式是 Integer.numberOfLeadingZeros(table.length) | (1 << (RESIZE_STAMP_BITS - 1))。通过这些位运算之后,其实 rs 的低 16 位就是当前扩容的线程数+1

下面看下transfer 方法做了什么:

  1. 根据 CPU 计算每个扩容线程分配的桶的数量
  2. 在一个 for循环中进行桶的迁移
    1. 首先是利用一个 while 循环,为当前线程分配自己需要迁移的桶的区间
    2. 跳出 while 循环后,首先判断本线程的是否还有迁移任务需要做,如果没有的话,需要判断本线程是不是最后一个负责迁移的线程,如果是的话,需要做一些收尾工作(置空成员变量 nextTable,更新成员变量 table 为新的数组等);否则,直接 reurn,结束本线程的 transfer 方法。
    3. 然后需要判断当前迁移的桶的头结点是否为null,为 null 的话,直接插入 ForwardingNode 进行占位
    4. 然后判断当前节点是否已经被迁移了,是的话直接跳过这个桶
    5. 如果当前节点需要进行迁移的话,先把桶的头结点锁掉,然后进行迁移
      链表迁移过程:
      1. 首先遍历链表,取到链表的尾结点,并得到尾结点是高位还是低位
      2. 然后通过头插法,产生分别由高位节点和低位节点组成的两个链表,然后把低位链表的头结点设置到下标为 i(i 是该节点在原数组的下标),把高位链表的头结点设置到下标为 i+n (i 是该节点在原数组的下标,n 是原数组的长度)

transfer 具体代码:

    private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        // stride 代表扩容时,每个扩容线程的「步」,即每个线程最大迁移的桶的数量
        int n = tab.length, stride;
        // 多核CPU情况下,stride = tab长度 * 8 / 核数,否则stride = table 长度,且stride 最小是 16
        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;
            // 下一个要迁移的索引(加一之后的值),等于 n 则表示,要从旧表的末尾开始迁移数据到新表
            transferIndex = n;
        }
        int nextn = nextTab.length;
        // 在扩容的过程中,如果有其他线程尝试进行读操作,那么通过 ForwardingNode 将读操作转发到新的 table 上去
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
        // 是否能够向前推进到下一个区间,首次为 true
        boolean advance = true;
        // 完成状态,如果是 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;
                // 如果当前区间还未处理完成,则将 advance 置为 false,跳出 while,继续处理当前区间
                if (--i >= bound || finishing)
                    advance = false;
                // 如果当前区间已经处理完成,而且不存在其他区间可以分配,则将 i 置为-1,将 advance 置为 false  
                else if ((nextIndex = transferIndex) <= 0) {
                    // 当 transferIndex <=0,则表示已经没有需要迁移的桶,这时候,将 i 置为 -1,准备退出迁移工作
                    i = -1;
                    advance = false;
                }
                // 如果当前区间已经处理完成,而且存在其他区间可以分配,则申请下一个可分配的区间,然后将 advance 置为false,跳出 while,进行迁移
                else if (U.compareAndSwapInt
                         (this, TRANSFERINDEX, nextIndex,
                          nextBound = (nextIndex > stride ?
                                       nextIndex - stride : 0))) {
                    // 将边界置为 nextIndex - stride 或者 0
                    bound = nextBound;
                    // 从最右边开始处理
                    i = nextIndex - 1;
                    advance = false;
                }
            }
            // i<0 代表没有需要迁移的桶
            // TODO i >= n ???
            // TODO i + n >= nextn ???
            if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                // 扩容完成
                if (finishing) {
                    // 清空成员变量
                    nextTable = null;
                    // 替换为新的 table
                    table = nextTab;
                    // 更新 sizeCtl
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }
                //表示当前线程迁移完成了
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                    // 如果当前线程是迁移工作不是最后一个线程,则直接 return,结束当前线程的工作
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    // 如果当前线程是迁移工作的最后一个线程,则将 finishing 标记置为true,标记整个迁移工作已经结束
                    finishing = advance = true;
                    i = n; // recheck before commit
                }
            }
            // 如果当前迁移桶的头结点为null,则直接使用 ForwardNode 进行占位
            else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);
            // 当前节点已经处理过了
            else if ((fh = f.hash) == MOVED)
                advance = true; 
            // 进行迁移工作
            else {
                // 对节点上锁,防止其他线程 putVal
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        // ln 高位桶;hn 低位桶,分别保存hash值的第X位为0和1的节点
                        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;
                                }
                            }
                            // 如果最后一个节点是低位,则将 lastRun 赋值给 ln,负责将 lastRun 赋值给 hn
                            if (runBit == 0) {
                                ln = lastRun;
                                hn = null;
                            }
                            else {
                                hn = lastRun;
                                ln = null;
                            }
                            // 遍历链表,根据高低位,以 ln,hn 为尾结点,进行头插
                            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);
                            }
                            // 设置低位 node 组成的链表的头结点
                            setTabAt(nextTab, i, ln);
                            // 设置高位 node 组成的链表的头结点
                            setTabAt(nextTab, i + n, hn);
                            // 原 table 的节点使用 FowardingNode 占位
                            setTabAt(tab, i, fwd);

                            advance = true;
                        }
                        // TODO 红黑树以后再补充
                        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;
                        }
                    }
                }
            }
        }
    }

transfer 方法迁移过程相关示意图:
在这里插入图片描述
扩容过程中的其他线程操作行为:

  1. put 方法
    • 需要帮助一起扩容,扩容完之后才能进行put操作
  2. get 方法
    • 借助volatile,不需要加锁。不过需要通过 ForwardingNode 转换到新的 table 上去进行 get 操作。

get 方法线程安全

get 方法不需要加锁,原因是 Node<K,V>[] table 和 Node对象中的 val 和 next 字段都是 volatile 修饰的,不存在脏读的问题。

size 方法是如何在其他线程插入的时候计算 size 的

size 机制建立在这两个成员变量上,相关方法是 addCount 和 size.

   private transient volatile long baseCount;
    private transient volatile CounterCell[] counterCells;

在 addCount 中,会选择首先 cas 更新 baseCount,如果操作失败,说明并发竞争比较高,这时候,将新增的数量放在 CounterCell 之中。size 方法计算时,则取 baseCount + (CounterCell[]中所有 value 的和) 作为 ConcurrentHashMap 的数量返回。

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

addCount 相关源码:

// TODO

参考

  • https://blog.csdn.net/ZOKEKAI/article/details/90051567
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值