关于java:基于JDK18的ConcurrentHashMap的源码的分析put操作和扩容

4次阅读

共计 9950 个字符,预计需要花费 25 分钟才能阅读完成。

ConcurrentHashMap

JDK1.8 摈弃了原有的 Segment 分段锁,而采纳了 CAS + synchronized 来保障并发安全性。
创立 ConcurrentHashMap 对象,应用一个参数的构造函数时做了什么?
应用 put 办法干了哪些事件?
怎么扩容的?
且带着疑难,依据源码一步步剖析。
在剖析之前,先明确几个重要的成员变量。

  • table: 数组,默认为 null, 用来存储 Node 节点数据,扩容时大小总是 2 的幂次方
  • nextTable: 默认为 null,扩容时新生成的数组,其大小为原数组的两倍
  • sizeCtl: 默认为 0,用来管制数组的初始化和扩容操作。正数示意有线程正在扩容。

ConcurrentHashMap 一个参数结构器的过程

// 带 1 个参数结构器
public ConcurrentHashMap(int initialCapacity) {
        // 小于抛出异样
        if (initialCapacity < 0)
            throw new IllegalArgumentException();
        // 对于给定的预期容量作出正当布局。注:MAXIMUM_CAPACITY 为 2 的 30 次方,MAXIMUM_CAPACITY >>> 1 为 2 的 30 次幂为 536870912,个别 initialCapacity 不会设置这么大的
        int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
                   MAXIMUM_CAPACITY :
                   tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1)); 
         //tableSizeFor 办法能够转换为 sizeCtl =【(1.5 * initialCapacity + 1),而后向上取最近的 2 的 n 次方】// 例如,initialCapacity 为 7,sizeCtl=cap=16
        this.sizeCtl = cap;
}

put 办法的过程

public V put(K key, V value) {
    // put 办法里间接去调用了 putVal 办法
    return putVal(key, value,false);
}

putVal 办法剖析

final V putVal(K key, V value, boolean onlyIfAbsent) {
    // key 键和 value 值不能为 null
    if (key == null || value == null) throw new NullPointerException();
    // 计算 hash 值, 将 Key 的 hashCode 值与其高 16 位作异或再按位与 int 的最大值从而保障最高位为 0(从而保障最终后果为正整数 )
    // 通过 spread 函数,int hash = (key.hashCode() ^ (key.hashCode() >>> 16)) & HASH_BITS
    // HASH_BITS=int 型的最大值, 即十六进制 0x7fffffff, 二进制 0111 1111 1111 1111 1111 1111 1111 1111
    int hash = spread(key.hashCode());
    // 局部变量,binCount 默认是 0, 只有 hash 抵触了才会大于 1. 且他的大小是链表的长度(如果不是红黑数构造的话)。int binCount = 0;
    // 循环, 因为前面是 CAS 操作,可能会须要大量的重试
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        // 如果没数组为空,调用 initTable 办法初始化创立数组
        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<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        // 如果 hash 抵触了,且 hash 值为 -1,阐明是 forwarding node 对象(这是一个占位符对象,保留了扩容后的容器)else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);// 帮忙数据迁徙
        else { // 这里就是数组曾经有元素了,这时候就该挂链表或者挂树了
            V oldVal = null;
            // 获取头节点的监视器锁
            synchronized (f) {if (tabAt(tab, i) == f) {
                    // 头节点的 hash 值, 大于 0 示意这上面有点货色
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) { //for 循环,示意遍历链表,循环一次后 binCount 加 1
                            K ek;
                            // 如果发现了 "相等" 的 key,判断是否要进行值笼罩
                            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;
                            // 到最初了没反复的 key,就把新值向前面挂
                            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;
                        // 插节点, 调用 TreeBin 的 putTreeVal 办法
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }

            if (binCount != 0) {
                // 判断链表的长度,如果大于 8,而后转树
                if (binCount >= TREEIFY_THRESHOLD)
                    // 这里要留神一个中央!!!!!-- 不是说像 HashMap 那样转树就没事了
                    // 这里波及到一个外围思路,CurrentHashMap 做了优化,这里如果数组长度小于 64,它会先扩容,扩容代表什么含意
                    // -- 原来的链表会被 1 分为 2 别离散落在不同的节点上
                    treeifyBin(tab, i);
                // 如果 key 已存在,有原值,返回原值
                if (oldVal != null)
                    return oldVal;
                break; // 完结外层死循环
            }
        }
    }
    // 元素计数加 1,依据 binCount 来测验是否须要检查和扩容
    addCount(1L, binCount);
    return null;
}

initTable 办法进行数组初始化

// 在下面的 putVal 源码里,当数组为空或长度为 0 的时候,需初始化,调用了 initTable() 办法
private final Node<K,V>[] initTable() {Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {// 须要留神的是,当整形的变量 sc(即 sizeCtl)小于 0,那么阐明有其余线程在在扩容,就调用线程的 yield() 办法
        if ((sc = sizeCtl) < 0)
            Thread.yield(); // lost initialization race; just spin
        // 应用 sun.misc.Unsafe 的 compareAndSwapInt 办法设置以后对象的 sizeCtl 为 -1,设置胜利后,初始化数组,默认容量 DEFAULT_CAPACITY 为 16
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {if ((tab = table) == null || tab.length == 0) {int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    // 变量 sc 的值为 (n-(n>>>2)),n-(n>>>2)=n-(n/2^2)=n-n/4=3/4*n=0.75*n, 默认 n 开始为 16,16*0.75=12
                    // 可知,负载因子为 0.75
                    sc = n - (n >>> 2);
                }
            } finally {sizeCtl = sc;}
            break;
        }
    }
    return tab;
}

链表转树,treeifyBin 办法剖析

private final void treeifyBin(Node<K,V>[] tab, int index) {
    Node<K,V> b; int n, sc;
    if (tab != null) {
        //MIN_TREEIFY_CAPACITY 为 64 
        // 尽管进入到转树办法,如果数组长度小于 64,那么先扩容
        if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
            tryPresize(n << 1); // 扩容,上面具体说
        // 确定头节点没问题开始加锁,转树
        else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {synchronized (b) {if (tabAt(tab, index) == b) {
                    TreeNode<K,V> hd = null, tl = null;
                    // 遍历链表,生成一棵树
                    for (Node<K,V> e = b; e != null; e = e.next) {
                        TreeNode<K,V> p =
                            new TreeNode<K,V>(e.hash, e.key, e.val,
                                              null, null);
                        if ((p.prev = tl) == null)
                            hd = p;
                        else
                            tl.next = p;
                        tl = p;
                    }
                    // 把数据放到树中
                    setTabAt(tab, index, new TreeBin<K,V>(hd));
                }
            }
        }
    }
}

tryPresize 办法进行扩容

private final void tryPresize(int size) {
    // 如果大小大于等于 MAXIMUM_CAPACITY(2 的 30 次幂)的一半,那么间接扩容为 MAXIMUM_CAPACITY,否则扩容为 1.5 的 size 加 1 再向上获取最近的 2 的整数次幂
    int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
            tableSizeFor(size + (size >>> 1) + 1);
    int sc;
    // 如果 sizeCtl 大于等于 0
    while ((sc = sizeCtl) >= 0) {Node<K,V>[] tab = table; int n;
        // 数组 table 为空
        if (tab == null || (n = tab.length) == 0) {n = (sc > c) ? sc : c;
            // CAS 批改 SIZECTL 为 -1,示意数组 table 正在进行初始化
            if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                // 确认其余线程没有对数组 table 批改
                try {
                    //
                    if (table == tab) {Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = nt;
                        // sc=n-n/4=0.75*n
                        sc = n - (n >>> 2);
                    }
                } finally {sizeCtl = sc;}
            }
        }
        // 数组 table 不为空,如果扩容大小没有达到阈值,或者超过最大容量 2 的 30 次方, 跳出 while 循环
        else if (c <= sc || n >= MAXIMUM_CAPACITY)
            break;
        else if (tab == table) {
            // 生成戳
            int rs = resizeStamp(n);
            if (sc < 0) {// 有线程在进行扩容
                Node<K,V>[] nt;
                /**1. 第一个判断 sc 右移 RESIZE_STAMP_SHIFT(16)位,也就是比拟高 ESIZE_STAMP_BITS(16)位生成戳和 rs 是否相等
                 * 相等则代表是同一个 n,是在同一节点下进行的扩容,*  2. 第二个和第三个判断 判断以后帮忙扩容线程数是否已达到 MAX_RESIZERS(2 的 16 次方 -1=65535)最大扩容线程数
                 *  3. 第四个和第五个判断 为了确保 transfer() 办法初始化结束
                 */
                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 值,作为扩容的发动,rs 左移 RESIZE_STAMP_SHIFT(16) 位 +2
             * 此时 sizeCtl 高 RESIZE_STAMP_BITS(16) 位为生成戳,低 RESIZE_STAMP_SHIFT(16) 位为扩容线程数
             */
            else if (U.compareAndSwapInt(this, SIZECTL, sc,
                    (rs << RESIZE_STAMP_SHIFT) + 2))
                // 挪动和拷贝节点到新数组
                transfer(tab, null);
        }
    }
}

挪动和拷贝节点到新数组,transfer 函数

// 该办法通过全局的 transferIndex 来管制每个线程的迁徙工作
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {// n 为旧 tab 的长度,stride 为步长 ( 就是每个线程迁徙的节点数)
    int n = tab.length, stride;
    // 单核步长为 1,多核为(n>>>3)/ NCPU, 最小值为 16
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range
    // 新的 table 尚未初始化
    if (nextTab == null) { // initiating
        try {
            // 扩容 2 倍
            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
            // 更新
            nextTab = nt;
        } catch (Throwable ex) {      // try to cope with OOME
            // 扩容失败,sizeCtl 应用 int 最大值
            sizeCtl = Integer.MAX_VALUE;
            return;
        }
        // nextTable 为全局属性
        nextTable = nextTab;
        // 更新转移下标,就是老的 tab 的 length
        transferIndex = n;
    }
    int nextn = nextTab.length;// 新 tab 的 length
    // 创立一个 fwd 节点,用于占位。当别的线程发现这个槽位中是 fwd 类型的节点,则跳过这个节点
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    // 首次推动为 true,如果等于 true,阐明须要再次推动一个下标(i--), 反之, 如果是 false, 那么就不能推动下标,须要将以后的下标处理完毕能力持续推动
    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;
        // 如果以后线程能够向后推动;这个循环就是管制 i 递加。同时,每个线程都会进入这里获得本人须要转移的桶的区间
        while (advance) {
            int nextIndex, nextBound;
            // 对 i 减一,判断是否大于等于 bound(失常状况下,如果大于 bound 不成立,阐明该线程上次支付的工作曾经实现了。那么,须要在上面持续支付工作)// 如果对 i 减一大于等于 bound(还须要持续做工作),或者实现了,批改推动状态为 false,不能推动了。工作胜利后批改推动状态为 true
            // 通常,第一次进入循环,i-- 这个判断会无奈通过,从而走上面的 nextIndex 赋值操作(获取最新的转移下标)// 其余状况都是:如果能够推动,将 i 减一,而后批改成不可推动。如果 i 对应的桶解决胜利了,改成能够推动
            if (--i >= bound || finishing)
                // 这里设置 false,是为了避免在没有胜利解决一个桶的状况下却进行了推动 这里的目标是:// 1. 当一个线程进入时,会选取最新的转移下标。// 2. 当一个线程解决完本人的区间时,如果还有残余区间的没有别的线程解决。再次获取区间。advance = false;
            else if ((nextIndex = transferIndex) <= 0) {
                // 如果小于等于 0,阐明没有区间了,i 改成 -1,推动状态变成 false,不再推动,示意,扩容完结了,以后线程能够退出了
                // 这个 -1 会在上面的 if 块里判断,从而进入实现状态判断
                i = -1;
                advance = false;// 这里设置 false,是为了避免在没有胜利解决一个桶的状况下却进行了推动
            }
            else if (U.compareAndSwapInt
                    (this, TRANSFERINDEX, nextIndex,
                            nextBound = (nextIndex > stride ?
                                    nextIndex - stride : 0))) {
                // 这个值就是以后线程能够解决的最小以后区间最小下标
                bound = nextBound;
                // 首次对 i 赋值,这个就是以后线程能够解决的以后区间的最大下标
                i = nextIndex - 1;
                // 这里设置 false,是为了避免在没有胜利解决一个桶的状况下却进行了推动,这样对导致漏掉某个桶
                // 上面的 if (tabAt(tab, i) == f) 判断会呈现这样的状况
                advance = false;
            }
        }
        // 如果 i 小于 0(不在 tab 下标内,依照下面的判断,支付最初一段区间的线程扩容完结)if (i < 0 || i >= n || i + n >= nextn) {
            int sc;
            if (finishing) {// 如果实现了扩容
                nextTable = null;// 删除成员变量
                table = nextTab;// 更新 table
                sizeCtl = (n << 1) - (n >>> 1); // 更新阈值
                return;
            }
            // 尝试将 sc -1. 示意这个线程完结帮忙扩容了,将 sc 的低 16 位减一
            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                // 如果 sc - 2 不等于标识符左移 16 位。如果他们相等了,阐明没有线程在帮忙他们扩容了。也就是说,扩容完结了。if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    return;// 不相等,阐明没完结,以后线程完结办法
                // 如果相等,扩容完结了,更新 finising 变量
                finishing = advance = true;
                i = n; // recheck before commit // 再次循环检查一下整张表
            }
        }
        // 获取老 tab i 下标地位的变量,如果是 null,就应用 fwd 占位。else if ((f = tabAt(tab, i)) == null)
            // 如果胜利写入 fwd 占位,再次推动一个下标
            advance = casTabAt(tab, i, null, fwd);
        else if ((fh = f.hash) == MOVED)// 如果不是 null 且 hash 值是 MOVED,MOVED=-1
            // 阐明别的线程曾经解决过了,再次推动一个下标
            advance = true; // already processed
        else {// 到这里,阐明这个地位有理论值了,且不是占位符节点。对这个节点上锁。为什么上锁,避免 putVal 的时候向链表插入数据
            synchronized (f) {
                // 判断 i 下标处的桶节点是否和 f 雷同
                if (tabAt(tab, i) == f) {
                    Node<K,V> ln, hn;// low, height 高位桶,低位桶
                    if (fh >= 0) {
                        // 对老长度进行与运算(第一个操作数的的第 n 位于第二个操作数的第 n 位如果都是 1,那么后果的第 n 为也为 1,否则 0)// 因为 Map 的长度都是 2 的次方(000001000 这类的数字),那么取于 length 只有 2 种后果,一种是 0,一种是 1
                        //  如果是后果是 0,Doug Lea 将其放在低位,反之放在高位,目标是将链表从新 hash,放到对应的地位上,让新的取于算法可能击中他
                        int runBit = fh & n;
                        // 尾节点,且和头节点的 hash 值取于不相等
                        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;
                            }
                        }
                        // 如果树的节点数小于等于 6,那么转成链表,反之,创立一个新的树
                        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;
                    }
                }
            }
        }
    }
}

总结

  • 应用了 CAS 加 synchronized 来保障了 put 操作并发时的危险(特地是链表)
  • 采纳了 数组 + 链表 + 红黑树 的数据结构
  • 单线程初始化,多线程协同扩容

FAQ:

  • 什么时候触发扩容?
  1. 链表转换为红黑树时 (链表节点个数达到 8 个会转换为树),数组长度小于 64
  2. 数组中总节点数大于阈值 (数组长度的 0.75 倍)
  • 如何 hash 定位

h^(h>>>16)&0x7fffffff,先将 hashCode 的高 16 位和低 16 位异或运算,这个做目标是为了让 hash 值更加随机。和 0x7fffffff(int 的最大值)相与运算是为了失去负数,因为正数的 hash 有非凡用处,如 - 1 表 forwarding node(示意该地位正在扩容)

  • 扩容时,扩容后的容量是原先的几倍?单线程扩容吗?
    2 倍,多线程协同扩容,每个线程负责一块区域的复制迁徙工作
正文完
 0