ConcurrentHashMap

123 阅读9分钟

JDK 1.7

  • JDK 1.7 中的 ConcurrentHashMap 最外层是多个 Segment,每个 Segment 的底层数据结构与 HashMap 类似,仍然是数组和链表组成的拉链法。
  • 每个 Segment 独立上 ReentrantLock 锁,每个 Segment 之间互不影响,提高了并发效率。
  • ConcurrentHashMap 默认有 16 个 Segments,所以最多可以同时支持 16 个线程并发写(操作分别分布在不同的 Segment 上)。这个默认值可以再初始化的时候设置为其他值,但是一旦初始化以后是不可以扩容的。
  • ConcurrentHashMap 不允许 key 为 null

JDK 1.8

  • ConcurrentHashMap 1.8 结构和 HashMap 类似
  • 初始化的时候通过自旋、 CAS 和双重 check 方法保证线程安全
  • put 的时候通过以下几点保证线程安全
    • 自旋死循环保证必须新增成功;
    • 指定位置上的槽点为空的时候,使用 CAS 生成第一个 Node,防止重复生成;
    • 槽点有值的时候,使用 synchronized 进行加锁,锁住当前位置的第一个 Node,如果是 TreeBin 锁住的就是整棵树;
    • 在增加节点并且需要重新维护红黑树的节点的时候,或者删除树节点的时候,需要 lock 住 TreeBin 的 lockState,网上都说是保证树维护平衡的操作出现并发情况,准确来说主要是用于 get 的时候判断使用 TreeBin 的查询方法还是使用链表的查询方法,因为基本上所有的维护平衡的操作都在 synchronized 里,所以写入不会冲突,但是树旋转的时候可能会改变根节点或者链接,而读操作永远不应该被阻塞,所以在维护红黑树平衡的时候锁住 lockState。
  • 树化 treeifyBin 方法,是使用 synchronized 与双重 check 保证线程安全,在没构建好树的时候,get 取得还是链表里的数据,会在构建好之后把链表替换掉
  • get 的几种情况
    • 是 TreeBin:
      • 当 lockState 是等待或者写(WAITER | WRITER)的时候,使用红黑树维护的链表进行查询,速度慢,但是读写不互斥,所以我认为 lockRoot 方法改变 lockState 就是用在这;
      • 否则就是使用树的查找操作,速度快,会加读锁,可能会唤醒因为读锁而阻塞的线程;
    • 是 ForwardingNode,到 nextTable 上查找;
    • 是链表,递归往后遍历,找需要的值。
  • size 方法的实现和 LongAdder 类似,有 cellsBusy 控制新增数据时,count 加在 baseCount 或者 counterCells 数组里,最后取 baseCount 和 counterCells 里的值加一块。
  • 扩容 transfer 的时候, 可以是多线程协助进行扩容,并且不会出现线程安全问题
    • 在 new Node 赋值给 nextTable 不会出现并发问题的原因:在所有能进入 transfer 方法之前会用 CAS 的方式把 sizeCtl 设置为 (rs << RESIZE_STAMP_SHIFT) + 2) ,而 rs 是 resizeStamp(n) 方法得到的,这个方法上写着“左移”RESIZE_STAMP_SHIFT 肯定为负,而为负的话,就只会进入协助扩容的代码里去。
    • 多线程扩容的时候:因为每个线程处理扩容是一批一批进行的,需要处理哪个位置到哪个位置的数据是从后往前处理,先分配再处理,分配到哪个位置用 transferIndex 用 CAS 的方式更新
    • 不会和新增删除出现并发问题:处理的时候用 synchronized 加锁指定下标的 Node
    • 不会阻塞读取操作:
      • 在新建了 ForwardingNode ,并且没获取到锁的时候
        • 可能新增删除拿到锁了,安全的原因看 put 方法
        • 可能没人拿到锁,还没执行到加锁,那 get 的时候按正常的 get 方法,用 ForwardingNode 转移到新数组上去查询
      • 拿到锁的时候
        • 指定位置还没设置成 ForwardingNode ,那么正常的 get
        • 设置成 ForwardingNode ,那么查询按照 ForwardingNode 的查询方法
  • 先写后读不阻塞、先读可能会阻塞另外线程的写,读读不阻塞,写写阻塞。

put 方法

    final V putVal(K key, V value, boolean onlyIfAbsent) {
        // key value 不允许为 null
        if (key == null || value == null) throw new NullPointerException();
        // 计算 hash 值
        // -1为ForwardingNode表示正在扩容,-2为TreeBin表示桶内为红黑树,大于0表示桶内为链表。)
        int hash = spread(key.hashCode());
        // 用来记录所在table数组中的桶的中链表的个数,后面会用于判断是否链表过长需要转红黑树
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            // 懒加载,没有初始化才初始化,进行下一次循环
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            // 已经初始化过,但是这个位置还没有任何 Node 数据
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                // CAS 设置 Node,成功就结束循环,否则下次循环
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            // 如果头节点 hash 值为-1,则为 ForwardingNode 节点,说明正在扩容
            else if ((fh = f.hash) == MOVED)
                // 调用hlepTransfer帮助扩容
                tab = helpTransfer(tab, f);
            // 目前槽点有值的往下执行
            else {
                V oldVal = null;
                // 锁定当前槽点 / 红黑树(TreeBin),每个槽点是独立的
                synchronized (f) {
                    // 再检查一次加锁之前,这个索引位置的槽点的头节点是否被修改了
                    if (tabAt(tab, i) == f) {
                        // 大于0 是链表
                        if (fh >= 0) {
                            // binCount 链表里是节点的个数
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                // 这个下标的 Node 链表里有同样的 key 的值,直接替换
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                // 新的 key,那么创建新的 Node 放在链表尾部,退出循环
                                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;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    // 大于阈值,有树化的条件,进去判断是否树化
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        // 添加 size ,可能会进行扩容
        addCount(1L, binCount);
        return null;
    }

putTreeVal 方法

        final TreeNode<K,V> putTreeVal(int h, K k, V v) {
            Class<?> kc = null;
            boolean searched = false;
            for (TreeNode<K,V> p = root;;) {
                int dir, ph; K pk;
                // 根节点为 null,创建根节点,退出循环
                if (p == null) {
                    first = root = new TreeNode<K,V>(h, k, v, null, null);
                    break;
                }
                // 当前节点的 hash 值大于要新增的节点,等会往左走
                else if ((ph = p.hash) > h)
                    dir = -1;
                // hash小于往右走
                else if (ph < h)
                    dir = 1;
                // key 相同直接返回了
                else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
                    return p;
                // 当前节点的 key 和要插入的 key hash 值相同,但是 equals 不成立
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0) {
                    if (!searched) {
                        TreeNode<K,V> q, ch;
                        searched = true;
                        // 递归寻找,得到了与更新的  key 相同的节点直接返回
                        if (((ch = p.left) != null &&
                             (q = ch.findTreeNode(h, k, kc)) != null) ||
                            ((ch = p.right) != null &&
                             (q = ch.findTreeNode(h, k, kc)) != null))
                            return q;
                    }
                    // 没找到,比较要插入的 key 和 当前节点的 key 返回 -1 或 1,表示左右子节点
                    dir = tieBreakOrder(k, pk);
                }

                // xp = 当前节点p
                TreeNode<K,V> xp = p;
                // 直到 当前节点p = 右边的或者左边的子节点,否则进行下次循环
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    TreeNode<K,V> x, f = first;
                    // 红黑树还维护了双向链表,每次新生成的 TreeNode 都会变成新的 头节点 first,因为查询的时候可能会使用链表形式查询
                    // 生成新的要添加的TreeNode节点,并且赋值给头节点first和x
                    first = x = new TreeNode<K,V>(h, k, v, f, xp);
                    // 如果原来的头节点不为空,把头节点挂到新增的节点后面
                    if (f != null)
                        f.prev = x;
                    // dir < = 0 ,把新增节点挂到它父节点(当前节点)的左子节点
                    if (dir <= 0)
                        xp.left = x;
                    // 否则把新增节点挂到它父节点(当前节点)的右子节点
                    else
                        xp.right = x;

                    // 如果新增节点的父节点是黑色,则新增节点为红色
                    if (!xp.red)
                        x.red = true;
                    // 如果新增节点的父节点是红色,则需要加锁重新维护红黑树的红黑平衡
                    else {
                        // 使用 CAS 锁住 lockState 还可能用上LockSupport
                        lockRoot();
                        try {
                            root = balanceInsertion(root, x);
                        } finally {
                            unlockRoot();
                        }
                    }
                    break;
                }
            }
            assert checkInvariants(root);
            return null;
        }

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) {
            // 与头节点 key 相同,返回结果
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            // 小于0,是红黑树(TreeBin)或者正在扩容(ForwardingNode)
            else if (eh < 0)
                // 调用TreeBin / ForwardingNode 的 find方法
                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;
    }

TreeBin.find 方法

        final Node<K,V> find(int h, Object k) {
            if (k != null) {
                for (Node<K,V> e = first; e != null; ) {
                    int s; K ek;
                    // 如果锁状态为 写或者等待,使用红黑树维护的链表进行查询
                    // 效率慢,但是读写不互斥
                    if (((s = lockState) & (WAITER|WRITER)) != 0) {
                        if (e.hash == h &&
                            ((ek = e.key) == k || (ek != null && k.equals(ek))))
                            return e;
                        e = e.next;
                    }
                    // 否则,使用红黑树的查询方法,查询速度快
                    // CAS lockState + 4
                    else if (U.compareAndSwapInt(this, LOCKSTATE, s,
                                                 s + READER)) {
                        TreeNode<K,V> r, p;
                        try {
                            // 红黑树查询方式
                            p = ((r = root) == null ? null :
                                 r.findTreeNode(h, k, null));
                        } finally {
                            Thread w;
                            // 减去自己线程的读锁
                            if (U.getAndAddInt(this, LOCKSTATE, -READER) ==
                                // 如果存在因为读锁而阻塞的线程,唤醒线程
                                (READER|WAITER) && (w = waiter) != null)
                                LockSupport.unpark(w);
                        }
                        return p;
                    }
                }
            }
            return null;
        }

transfer 方法

  ForwardingNode:主要是在扩容的时候,新数组的数据还没有完全设置完毕,那么就不可以把 ConcurrentHashMap 的 table 设置成新数组,这个时候查询的时候是通过 ForwardingNode 转移到新数组上去查询。

  扩容原理更详细的博文:blog.csdn.net/ZOKEKAI/art…

入口

  1. 每次添加完后,调用的 addCount 中有调用 transfer 扩容
  2. 桶中链表大于 8 调用 treeifyBin 方法转红黑树的方法的时候,在该方法中会判断 table 当前总容量是否大于64,如果 table 当前总容量小于 64,不会转红黑树,而是调用 tryPresize 方法尝试扩容,tryPresize 方法中会调用 transfer 扩容
  3. 在 putVal 发现 头节点的 hash 为 MOVED(-1)也就是这个节点为 ForwardingNode 的时候,会调用 helpTransfer 然后 transfer 方法扩容

addCount 方法

    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)) {
            ...
        }
        if (check >= 0) {
            Node<K,V>[] tab, nt; int n, sc;
            // 当前容量大于扩容阈值,并且长度小于最大限制,进行扩容
            // sizeCtl 在进行扩容完成之后的作用就是存放扩容阈值,而如果在扩容的时候,sizeCtl是一个负数,也会满足条件
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                   (n = tab.length) < MAXIMUM_CAPACITY) {
                /**
                 * Returns the stamp bits for resizing a table of size n.
                 * Must be negative when shifted left by RESIZE_STAMP_SHIFT.
                 * 左移 RESIZE_STAMP_SHIFT 肯定为负
                 */
                int rs = resizeStamp(n);
                // 小于0,表示有其他线程正在扩容
                if (sc < 0) {
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                }
                // CAS 的方式设置 sizeCtl 为一个负数
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    // 扩容
                    transfer(tab, null);
                s = sumCount();
            }
        }
    }

transfer 方法

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
        // 计算每条线程处理的桶个数,每条线程处理的桶数量一样,如果是单核CPU,则使用一条线程处理所有通
        // 每条线程最少处理 16 个桶,如果计算出来的结果少于 16,则一条线程处理 16 个桶
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; // subdivide range
        // 单线程扩容 或者 第一条线程,nextTab 肯定为 null。协助线程是不会满足条件的
        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
            nextTable = nextTab;
            // 扩容总进度,指向原数组索引最大的位置
            transferIndex = n;
        }
        int nextn = nextTab.length;
        // 这个节点的 hash 固定是 MOVED(-1)
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
        boolean advance = true;
        boolean finishing = false; // to ensure sweep before committing nextTab
        // bound 是这次任务的最后一个位置,即自己要处理的最小的下标,i 是在分配任务之后最大的下标数,即开始的位置
        // 是从后往前处理
        for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;
            // 1. 分配任务,这个线程负责转移哪几个位置的桶
            while (advance) {
                int nextIndex, nextBound;
                // 往前推进一个位置,然后进行判断是否到了自己要处理的最前面那个位置
                // 如果到了或者 finishing 标识为为 true,表示处理完了,没有到继续处理 i  位置
                if (--i >= bound || finishing)
                    advance = false;
                // transferIndex <= 0 说明被分配完了,结束 while 循环
                else if ((nextIndex = transferIndex) <= 0) {
                    i = -1;
                    advance = false;
                }
                // 只有首次进入 for 循环才会进入这个判断,设置了 i 和 bound 的值
                // 比如 transferIndex 本来是 100 ,stride 是 16 ,那么该线程领取到的任务就是 84 ~ 99 位置的槽点的转移任务
                // transferIndex 就被用 CAS 的方式设置成 84
                // 下次来了协助线程,继续分配 68 ~ 83 位置的槽点
                // 范围为 [bound, nextIndex)
                else if (U.compareAndSwapInt
                         (this, TRANSFERINDEX, nextIndex,
                          nextBound = (nextIndex > stride ?
                                       nextIndex - stride : 0))) {
                    bound = nextBound;
                    // 下标从 0 开始,所以 - 1
                    i = nextIndex - 1;
                    advance = false;
                }
            }

            // n 是原数组的长度,nextn 是新数组的长度
            // i < 0 表示这个线程拿到的范围是 [0, X) 并且执行完了或者没拿到(包括自己的执行完了,然后再次 while 循环没拿到任务)
            if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                // 所有线程干完活了
                if (finishing) {
                    nextTable = null;
                    // 替换新的 table
                    table = nextTab;
                    // 调 sizeCtl 为旧容量的 1.5 倍
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }
                // 到这就是表示这个线程的任务完成了  sizeCtl - 1,表示参与扩容的线程 -1
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                    /**
                     第一个扩容的线程,执行transfer方法之前,会设置 sizeCtl = (resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2)
                     后续帮其扩容的线程,执行transfer方法之前,会设置 sizeCtl = sizeCtl+1
                     每一个退出transfer的方法的线程,退出之前,会设置 sizeCtl = sizeCtl-1
                     那么最后一个线程退出时:
                     必然有sc == (resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2),即 (sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT
                     */
                    //不相等,说明不到最后一个线程,直接退出transfer方法
                    // 到最后一个线程就设置 finishing 为 true 下次循环的时候 用新 hash 表覆盖旧 hash 表
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    finishing = advance = true;
                    i = n; // recheck before commit
                }
            }
            // 这个位置没有任何 Node 设置一个占位对象
            else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);
            // hash 为 MOVED 说明已经被处理,继续回去执行第 1 步,分配任务
            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);
                            }
                            // 将 ln 设置到新数组下标为 i 的位置上
                            setTabAt(nextTab, i, ln);
                            // 将 hn 设置到新数组下标为 i +n 的位置上
                            setTabAt(nextTab, i + n, hn);
                            // 把刚才创建的ForwardingNode设置到原来的的hash桶上
                            // 作用是标识该位置已经被处理过,以及后来查询的转发作用
                            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) {
                                    // 如果 p 点没有前置节点
                                    if ((p.prev = loTail) == null)
                                        lo = p;
                                    // 如果 p 有前置节点那么把 loTail 的下一个节点设置为 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;
                        }
                    }
                }
            }
        }
    }