【Java8源码分析】并发包-ConcurrentHashMap(二)

转载请注明出处:http://blog.csdn.net/linxdcn/article/details/72871732


上一篇并发包-ConcurrentHashMap(一)主要介绍了ConcurrentHashMap的存储结构以及容器的存取方法的源码。



ConcurrentHashMap主要采用上图所示的结构存储元素,在插入第一个元素时采用CAS算法,其余的插入、更新、删除操作均以第一个元素作为锁操作。

本篇主要介绍以下ConcurrentHashMap的size函数以及扩容方法


1 size函数

在ConcurrentHashMap容器中不再在维护一个全容器容量的变量,故每次调用size函数时,需对每个桶的元素个数进行累加,首先来看看几个重要的属性域

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable {

    // 没有竞争条件时,添加一个元素会直接更新这个值
    private transient volatile long baseCount;

    // 有竞争时,则更新对应的CounterCell
    private transient volatile CounterCell[] counterCells;

    // 在扩容和创建CounterCells时使用
    private transient volatile int cellsBusy; 
}

在ConcurrentHashMap中有两个变量,一个是baseCount,当添加一个元素时,都会尝试去更新这个值。如果更新baseCount失败,才采用CAS算法把值随机更新到counterCells中去。counterCells是一个数组。调用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函数就是把baseCount和counterCells的数值全部相加,获得那个时刻的容器大小。下面来看看容器是如何更新这两个变量的,当添加一个元素是,都会调用addCount这个函数。

private final void addCount(long x, int check) {
    CounterCell[] as; long b, s;
    // 如果counterCells未初始化,或则更新baseCount失败,即有其他线程竞争
    // 则将大小值保存在counterCells中
    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 ||
            // 在counterCells随机找个位置存放
            (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
            !(uncontended =
              U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
            // 如果更新counterCells也失败了,则调用fullAddCount
            // 若counterCells == null,则先初始化
            // 然后轮流更新counterCells或baseCount
            fullAddCount(x, uncontended);
            return;
        }
        if (check <= 1)
            return;
        s = sumCount();
    }
}

2 扩容函数

ConcurrentHashMap扩容的核心函数是transfer,当有其他线程发现该容器在扩容时,会调用helpTransfer来帮助扩容。扩容的内容是申请一个新的2倍大小的数组,将原来数组的元素复制到新数组中。下面是transfer函数的源码解释:

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
    // 如果nextTab为null,则初始化nextTab
    if (nextTab == null) {            // initiating
        try {
            @SuppressWarnings("unchecked")
            // 构造一个nextTable对象 它的容量是原来的两倍 
            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
            nextTab = nt;
        } catch (Throwable ex) {
            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;
    // 循环处理所有hash桶
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;
        // 找到需要迁移的hash桶
        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;
            // 如果所有的节点都已经完成复制工作
            // 就把nextTable赋值给table 清空临时对象nextTable 
            if (finishing) {
                nextTable = null;
                table = nextTab;
                sizeCtl = (n << 1) - (n >>> 1);
                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
            }
        }
        // 如果遍历到的节点为空,则放入ForwardingNode指针  
        else if ((f = tabAt(tab, i)) == null)
            advance = casTabAt(tab, i, null, fwd);
        // 如果遍历到ForwardingNode节点,说明这个点已经被处理过了 直接跳过
        // 这里是控制并发扩容的核心 
        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;
                    }
                }
            }
        }
    }
}

元素复制过程跟HashMap是一样的,因为扩容为2的倍数,根据hash桶的计算方法,元素哈希值不变,所以元素在新的hash桶的下标,要不跟旧的hash桶下标一致,要不增加oldSize。


3 总结

  • 为什么采用baseCount和counterCells数组来实现size?在高并发情况下,多个线程对同一个变量的修改,很容易就造成性能的严重下降,为了避免这种问题,就给每个hash桶分配一个计数的变量。
  • size函数并不保证返回准确的容器大小,因为很有可能在size函数累加各个hash桶数量过程中,某个hash桶的容量就发生了变化。在多线程环境中,一般也只需统计一下大概的容量大小即可。
  • 扩容方法的触发:(1)当添加一个元素,使容器大小超过最大限度 * 装载因子时;(2)当添加一个元素,一个hash桶达到树化限度(默认8),且容器容量未超过最小树化容量(默认64)。

转载请注明出处:http://blog.csdn.net/linxdcn/article/details/72871732

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页