(三)更好的并发-ConcurrethashMap

171 阅读7分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第5天,点击查看活动详情。 hashmap不安全,hashtable性能低,故而concurrent hashmap应运生。Java 7用分段锁,Java 8主要用CAS来保证并发安全。

Java 7 的实现

新的创新方式说采用Segment来实现,第一次哈希到segment,再次哈希到具体桶。Segment继承可重入锁,也就是每个分段一个锁,并发修改操作的时候先试自旋再加锁阻塞。 如下图所示:

java7-hashmap.png

  • 1、a c的修改可以并发,不冲突
  • 2、segment1 和segment 16的扩容相互独立

也就是说每个Segment的数组单独put/扩容,不会冲突。

稍加创新的Segment减小了put锁粒度,解决了hashtable并发效率低的问题。但是还不够完美,只是非常粗暴的分成N个段(比如16),每个段独自管理。 而Java8在处理ConcurrethashMap的时候做了非常精妙的设计,下面来看看。

Java 8 的优化

Java 8继续优化ConcurrethashMap,主要是采用CASVolatile技术的帮助。

基本数据结构

存储节点

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    volatile V val;
    volatile Node<K,V> next
  • volatile的val和next

put

put的逻辑还算清晰(但仍需要足够的耐心和跳跃思维),下面看看put的具体实现,再总结其设计特点。

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

注意:下面将源码中的bin翻译为🪣

final V putVal(K key, V value, boolean onlyIfAbsent) {
    // 和hashtable一样,不支持null
    if (key == null || value == null) throw new NullPointerException();
    // 二次哈希,先不管
    int hash = spread(key.hashCode());
    //桶里面的节点个数
    int binCount = 0;
    //while循环,每次都重置为table
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
           //tab得到初始化的表
            tab = initTable();
     
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
        // 如果这个hash所对应的桶为空,则CAS将这个key,value放到这个桶里
            if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))
                break;                   // 如果CAS成功,这次put就愉快的结束了
            //如果CAS失败,说明有其他抢先put这个桶成功了,则会继续跳转到 for (Node<K,V>[] tab = table;;) 执行
        }
        //f是桶的链表头,MOVED是-1,可是哈希值怎么会是-1?怎么不用sizeCtl值来判断这个是否合适,因为只需要帮忙某一个桶
        else if ((fh = f.hash) == MOVED)//普通Node的哈希值保证不会为-1?
            // 正在扩容中,那么先帮忙,tab被重新赋值为扩容后的表
            tab = helpTransfer(tab, f);
            //扩容完之后,则会继续跳转到 for (Node<K,V>[] tab = table;;) 执行
        else {//命中的桶不为空
            V oldVal = null;
            synchronized (f) {//f是桶的链表头,同步锁住更新
                if (tabAt(tab, i) == f) {//这个桶的初始值还没被更改
                    if (fh >= 0) {//不是特殊状态
                        binCount = 1;//在桶的链表的位置
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            if (e.hash == hash &&//hash再次判断
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                 //找到e的key==key(基本类型),或对象用equals判断
                                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) {//红黑树的fh值=TREEBIN,是-2
                    //f是红黑树的根节点

                        Node<K,V> p;
                        binCount = 2;//为什么binCount=2
                        if ((p = ((TreeBin<K,V>)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;//返回old value
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

看完这段代码,我们可以画出一个流程图,如下:

put.png

初始化表

初始化表的实现如下

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
        if ((sc = sizeCtl) < 0)
            Thread.yield(); // 如果已经在初始化了,就让出cpu等待
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {//CAS设置状态值
            try {
                if ((tab = table) == null || tab.length == 0) {
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;//sc>0是什么场景?
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];//分配内存
                    table = tab = nt;//table设置完
                    sc = n - (n >>> 2);//设置sc
                }
            } finally {
                sizeCtl = sc;//设置扩容的阈值
            }
            break;//成功初始化
        }
    }
    return tab;
}

无标题流程图.png

  • sizeCtl<0表示是有可能在初始化中

    • 负数表示在初始化或者扩容之中,-1初始化,-(1+n)扩容的线程数
  • 不然CAS设置sizeCtl值为-1,开始初始化

    • Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]分配内存

    • table=tab=nt

      • transient volatile Node<K,V>[] table;table也是volatile的,其他线程能马上看到。
    • sc=n-(n>>>2)的目的是用来设置扩容的阈值,n>>>2 无符号左移动两位,这个是除以4的意思

    • 最后将sizeCtl的值设置为sc。这样初始化之后,sizeCtl的值为正数,表示下次扩容的阈值size

      • private transient volatile int sizeCtl;可以看到sizeCtl也是volatile的,保证多线程可见性
    • 最后返回一个tab

    有个问题没有想清楚, 即使是第二次判断table==null之后,为什么这里finally要设置sizeCtl = sc?只是代码方便,还是有什么用意?不过有的时候只是一种保证不出错的写法,一定要问个究竟可能有的钻牛角尖。

Java8 扩容

put操作会判断,如果这个桶的头节点的hash已经是moved,那么put到这个hash桶的线程会来帮助扩容。 put之后,会判断是否超过阈值,如果超过则需要扩容。 这里还使用到了ForwardingNode,在扩容期间插入到链表头来帮助扩容到工具Node(哈希值设置为-1)

static final class ForwardingNode<K,V> extends Node<K,V> {
    final Node<K,V>[] nextTable;
    ForwardingNode(Node<K,V>[] tab) {
        super(MOVED, null, null, null);
        this.nextTable = tab;
    }
  • nextTable是ForwardingNode的table,也就是扩容一倍的初始化好的新的table

helpTransfer

final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
   Node<K,V>[] nextTab; int sc;
   if (tab != null && (f instanceof ForwardingNode) &&
   (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
      //resize标记
       int rs = resizeStamp(tab.length);
       while (nextTab == nextTable && table == tab &&
              (sc = sizeCtl) < 0) {
            //sc == rs + 1
           //或者 sizeCtl == rs + 65535 (如果达到最大帮助线程的数量,即 65535)
           //transferIndex <= 0,表示扩容完成
           if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
               sc == rs + MAX_RESIZERS || transferIndex <= 0)
               break;
            //帮助扩容,增加扩容的线程数量
           if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
               transfer(tab, nextTab);
               break;
           }
       }
       return nextTab;
   }
   return table;
}

(扩容结束了,不再有线程进行扩容)(默认第一个线程设置 sc ==rs 左移 16 位 + 2,当第一个线程结束扩容了,就会将 sc 减一。这个时候,sc 就等于 rs + 1) // 或者 sizeCtl == rs + 65535 (如果达到最大帮助线程的数量,即 65535)

主要的实现是在transfer方法。

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
    //通过计算 CPU 核心数和 Map 数组的长度得到每个线程(CPU)要帮助处理多少个桶
    //并且这里每个线程处理都是平均的。
    //默认每个线程处理 16 个桶。因此,如果长度是 16 的时候,扩容的时候只会有一个线程扩容
    if (nextTab == null) {            // 初始化
        try {
            @SuppressWarnings("unchecked")
            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;//更新转移下标全局变量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;
        //advance表示正在进行推进的这样一个过程
        while (advance) {
            int nextIndex, nextBound;
            //进入一个 while 循环,分配数组中一个桶的区间给线程,默认是 16.
            //从大到小进行分配。当拿到分配值后,进行 i-- 递减。这个 i 就是数组下标。
            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;//设置本线程的bound(分配一段空间),开始工作
                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
            }
        }
        //没什么可转移的,直接设置fwd标志
        else if ((f = tabAt(tab, i)) == null)
            advance = casTabAt(tab, i, null, fwd);
        else if ((fh = f.hash) == MOVED)
            advance = true; //已经被转移的情况,则不处理,如果两个线程并发transfer,[bound,transferIndex]范围是会存在一些重叠的区间的。
        else {
            synchronized (f) {//对桶的链表头加锁
                if (tabAt(tab, i) == f) {
                    Node<K,V> ln, hn;
                    if (fh >= 0) {//再次确认没有被转移,且不是红黑树
                    //对原来的hash和n(2^n)进行&运算, 结果只有两种情况,0和n
                        int runBit = fh & n;
                        Node<K,V> lastRun = f;
                        for (Node<K,V> p = f.next; p != null; p = p.next) {
                        //对原来的hash和n(2^n)进行&运算, 结果只有两种情况,0和n
                            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变成一个链表
                                ln = new Node<K,V>(ph, pk, pv, ln);
                            else//头插法将hn变为一个链表
                                hn = new Node<K,V>(ph, pk, pv, hn);
                        }
                        // 设置低位链表放在新链表的i
                        setTabAt(nextTab, i, ln);
                        //设置高位链表放在心链表的i+n
                        setTabAt(nextTab, i + n, hn);
                        //将旧链表设置为占位符
                        setTabAt(tab, i, fwd);
                        //继续向前推进
                        advance = true;
                    }
                    else if (f instanceof TreeBin) {//红黑树的情况t
                        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;
                    }
                }
            }
        }
    }
}

get操作

public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    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;//链表头的key就已经等了
        }
        else if (eh < 0)//小于0则是红黑树,可能是红黑树
            return (p = e.find(h, key)) != null ? p.val : null;
        while ((e = e.next) != null) {//依次沿着链表往下找,如果是moved节点,可能是在nextTable里面寻找。
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}

依次沿着链表往下找,如果是moved节点,可能是在nextTable里面寻找,这也不会影响get正确的获取到值