ConcurrentHashMap源码分析(详解扩容)

158 阅读11分钟

1、putVal()

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();//检查
    int hash = spread(key.hashCode());  //计算key的hash值
    int binCount = 0;//记录哈希桶节点的个数
    for (Node<K,V>[] tab = table;;) { //tab赋值
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0) //tab == null且length == 0,初始化哈希表 注意这里n也赋值了length
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { //按位与运算后给f赋值hash槽位 如果槽位为空
            if (casTabAt(tab, i, null,                      //cas的方式直接插入新节点
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        else if ((fh = f.hash) == MOVED) //MOVED 表示正在扩容
            tab = helpTransfer(tab, f); //帮助扩容迁移
        else {
            V oldVal = null;
            synchronized (f) { //加锁对象是f  f只是对应一个下标的哈希槽 锁的范围很小
                if (tabAt(tab, i) == f) { //再检查一下
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) { //遍历链表 binCount记录了此链表节点数量 是为了判断链表是否过长 是否需要树化
                            K ek;
                            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;
                        }
                    }
                }
            }
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD) //bincount >= 8 树化
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal; //是更新操作则返回旧值
                break;
            }
        }
    }
    addCount(1L, binCount); //更新计数器、检查扩容
    return null; //非更新操作 返回null
}

2、addCount() 累加计数器和检查扩容

  • 我们以元素个数为11个,表长度为16为例。也就是说sizeCtl = 12
private final void addCount(long x, int check) { //x是1,check是链表长度 但好像不是很重要
    CounterCell[] as; long b, s;
    //无竞争状态下counterCells == null 且 CAS baseCount = 11 + 1(累加1)成功,不会进入下面代码块
    //注意s也赋值了,变成了11+1=12,下面检查扩容会用
    //有竞争进入则进入以下代码块
    //内部实现后面再说,是longaddr的思想
    if ((as = counterCells) != null ||
        !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
        CounterCell a; long v; int m; //CAS失败、存在竞争
        boolean uncontended = true;
        if (as == null || (m = as.length - 1) < 0 ||
            (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
            !(uncontended =
              U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
            fullAddCount(x, uncontended);
            return;
        }
        if (check <= 1)
            return;
        s = sumCount();// 更新计数器的总和
    }
    if (check >= 0) { //检查扩容
        Node<K,V>[] tab, nt; int n, sc;
        //如果元素个数大于等于sizeCtl,需要扩容。此时12>=12
        while (s >= (long)(sc = sizeCtl) && (tab = table) != null && 
               (n = tab.length) < MAXIMUM_CAPACITY) {
            //计算扩容戳,并左移16位
            int rs = resizeStamp(n) << RESIZE_STAMP_SHIFT;
            // 如果 sizeCtl 小于 0,表示当前正在进行扩容操作
            // 第一次尝试扩容是不会进入下面这个if的
            if (sc < 0) {
                // 为负数时,低位储存扩容的线程数
                // 如果sc == 扩容戳 + 最大扩容线程,表示扩容的线程数已经到最大值了。
                // sc == rs + 1,表示扩容已经结束了,但正在进行重新检查原table的善后阶段,为什么这么说,带着这个问题去读接下来的源码
                // nextTable表示扩容用的新table,如果它为空表示没有在进行扩容或者已经扩容结束
                // transferIndex表示扩容索引,如果小于等于0表示没有在进行扩容操作或扩容结束
                // 满足四种情况之一会break
                if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
                    (nt = nextTable) == null || transferIndex <= 0)
                    break;
                    // 尝试增加扩容线程,sizectl低位加1
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                    transfer(tab, nt);
            }
            // 不是正在扩容的情况下走到这里,表示当前没有进行扩容操作,尝试发起扩容操作
            // CAS sizectl CAS成功后变成负数 表示正在扩容
            // sizectl 变成 rs+2 ,rs是扩容戳,为什么是rs+2而非rs+1,带着这个问题去读接下来的源码
            else if (U.compareAndSwapInt(this, SIZECTL, sc, rs + 2))
                transfer(tab, null); //扩容
            s = sumCount();// 更新计数器的总和
        }
    }
}

3、transfer() 计算步长

  • 注:代码是连续的,但是我们根据每段代码的作用分段看
//NCPU表示系统当前可用的处理器数量 这里以16为例
static final int NCPU = Runtime.getRuntime().availableProcessors();

//注意参数,nextTab表示扩容用的新table,第一次扩容nextTab是null
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;  //我们的n = 16
    //这里的步长可以理解为每次迁移的范围
    //最小步长MIN_TRANSFER_STRIDE = 16
    //可以自己分别代入一个NCPU比较大的数字,或者代入一个n(也就是原table长度)比较大的数字
    //处理器数量较小或table较小的时候stride基本等于最小步长
    //处理器数量多了,table大后步长会多一点。
    //如果处理器数量只有一个的话,步长就是table长度,也就是单线程情况下一次迁移完整个table
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range
}

4、transfer() 初始化nextTable(针对第一个来扩容的线程)

if (nextTab == null) {            // initiating 初始化nextTab
    try {
        @SuppressWarnings("unchecked")
        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]; //新table的长度是原table长度 * 2
        nextTab = nt;
    } catch (Throwable ex) {      // try to cope with OOME
        sizeCtl = Integer.MAX_VALUE;
        return;
    }
    //这两个成员变量都是被volatile修饰的,保证不同线程操作扩容时的可见性
    nextTable = nextTab;
    transferIndex = n;//用于控制迁移的位置 这里n = 16
}

5、transfer() 分配任务

  • 先看看ForWardingNode
ForwardingNode(Node<K,V>[] tab) {
    super(MOVED, null, null, null);
    this.nextTable = tab;
}
Node(int hash, K key, V val, Node<K,V> next) {
    this.hash = hash;
    this.key = key;
    this.val = val;
    this.next = next;
}

this.hash被赋值成了MOVED,表示已经迁移,MOVED就是-1,后面会见到这个MOVED

int nextn = nextTab.length; //新table长度 这里是32
//当扩容过程中遇到需要移动的节点时,可以将这些节点替换为 ForwardingNode,从而标识该位置正在进行扩容操作
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;;) { //初始时i = 0 bound = 0 finish = false
    Node<K,V> f; int fh;
    while (advance) {  //寻找此次迁移任务的边界
        int nextIndex, nextBound;
        // 第一次不会进入这个if块
        // 看开始迁移的代码,遇到advance变回true再看这个if块,他是控制迁移的索引的
        if (--i >= bound || finishing)
            // 如果 i 大于等于 bound ,迁移任务还没结束,advance重新变为false 继续走迁移的逻辑
            advance = false;
        //初始时transferIndex = 16 ,也不会进入这里,但注意nextIndex被赋值了
        else if ((nextIndex = transferIndex) <= 0) {
            // 扩容结束
            i = -1;
            advance = false;
        }
        // 第一次进入的是这里
        // 由TRANSFERINDEX直接控制迁移任务的工作区间
        // 本次的示例是table长为16,transferIndex = 16。如果是32的话分配的就是[16,31]了,另一个线程进来分到[0,15]
        else if (U.compareAndSwapInt
                 (this, TRANSFERINDEX, nextIndex,
                  nextBound = (nextIndex > stride ?    // 16 > 16 ? 16 - 16 : 0,这里nextBound为0
                               nextIndex - stride : 0))) {
            bound = nextBound; //bound赋值
            i = nextIndex - 1; //i = 15
            advance = false;
        }
    }
  • 第一次分配结束 bound = 0,i = 15,所以此次迁移任务的区间就是[0,15]

6、transfer() 开始迁移

// 如果i < 0 分俩种情况
// 1. 表示迁移结束,初始时i = 0,(上一块代码的if块)在第一个if块自减为-1,但没有进入最后一个if块分配到迁移区间,这时i仍然为-1
// 2. 
// i >= n n是原数组长度,迁移任务区间超出了table范围,不会成立
// i + n >= nextn nextn是新table的长度,i的最大值就是原数组最大索引,也不可能成立
// 所以我们先看后面的if块,这里的if块一会讲
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
    }
}
// 如果原table的索引i处为null,将此处替换为fwd表示已经迁移(如果不为null会走下面synchronized里的逻辑)
else if ((f = tabAt(tab, i)) == null) //==null
    // 替换成功则advance为true,表示一个索引处迁移结束
    // advance为true后就又可以进入分配任务的while循环中了
    // 再去看看分配任务的第一个if块,通过--i >= bound控制下一次迁移索引
    advance = casTabAt(tab, i, null, fwd); 
else if ((fh = f.hash) == MOVED) //MOVED 表示此索引处的元素已经被迁移
    //同样的advance变为true,进行i的自减
    advance = true; // already processed
else {
    //数据不为null情况下的迁移逻辑,和hashmap差不多,不说了,我们只关注迁移后的advance = true
    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;
            }
        }
    }
}

7、迁移结束的善后处理以及rs + 2 & sc + 1的解释

  • 当i = -1后,会进入迁移结束的if块
if (i < 0 || i >= n || i + n >= nextn) {
    int sc;
    //最开始进来finish仍然为false
    //善后工作
    if (finishing) {
        nextTable = null;
        table = nextTab; //更新table
        sizeCtl = (n << 1) - (n >>> 1); //计算新的阈值
        return;
    }
    //毫无疑问会先执行这个CAS,他将sizeCtl减了1,sizeCtl低位表示扩容的线程数量,这表示一个线程结束了它的迁移扩容任务
    //还记得在addCount中有一个sc == rs + 1的判断吗,以及为什么最开始sc = rs + 2 而非 rs + 1
    if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
        //情况:如果 (sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT,表示sizeCtl被操作过了,迁移过程中有别的线程也来帮助迁移了。
        //可以思考一下,最终更新table和sizectl的操作应该由哪个线程进行,显然应该是执行最后一个迁移任务的线程
        //别的线程迁移结束后纷纷将sizeCtl - 1,最后一个线程sizeCtl - 2刚好就是迁移戳resizeStamp(n) << RESIZE_STAMP_SHIFT
        if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
            return; //非最后一个线程,直接return
            
        //最后一个线程才会进入这里,处理善后工作
        finishing = advance = true;
        i = n; // recheck before commit // i = 原数组长度,重新走--i>=bound的逻辑,重新检查一遍
        // 这里的重新检查实际上是判断原table是不是全部都是MOVED了,如果不是则再迁移一次
        // 检查完后才进入if(finish)中
    }
}

8、helpTransfer()

在putVal操作时有时会遇到访问的下标正在扩容的情况

else if ((fh = f.hash) == MOVED) //MOVED 表示正在扩容
    tab = helpTransfer(tab, f); //帮助扩容迁移
  • 前面看懂了这块代码应该很容易理解了
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
    Node<K,V>[] nextTab; int sc;
    // 检查条件:tab 不为空,f 是 ForwardingNode,并且下一个表不为空
    if (tab != null && (f instanceof ForwardingNode) &&
        (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
        // 计算扩容戳
        int rs = resizeStamp(tab.length) << RESIZE_STAMP_SHIFT;
        // 二次检查处于扩容状态
        while (nextTab == nextTable && table == tab &&
               (sc = sizeCtl) < 0) {
               //如果扩容线程已满或扩容已经进入善后阶段,不扩容
            if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
                transferIndex <= 0)
                break;
                //低位+1,一起扩容
            if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                transfer(tab, nextTab);
                break;
            }
        }
        return nextTab;
    }
    return table;
}

9、扩容时机2

  • 长链表时扩容
if (binCount != 0) {
    if (binCount >= TREEIFY_THRESHOLD) //bincount >= 8 树化
        treeifyBin(tab, i);
    if (oldVal != null)
        return oldVal; //是更新操作则返回旧值
    break;
}
private final void treeifyBin(Node<K,V>[] tab, int index) {
    Node<K,V> b; int n, sc;
    if (tab != null) {
        // 如果哈希表的容量小于树化阈值
        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));
                }
            }
        }
    }
}

10、tryPresize()

private final void tryPresize(int size) {
    // tableSizeFor() 找size的最近二次幂
    int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
        tableSizeFor(size + (size >>> 1) + 1);
    int sc;
    while ((sc = sizeCtl) >= 0) {
        Node<K,V>[] tab = table; int n;
        // 这块代码是针对putAll的,大体就是根据传进来的size初始化了一个size大小的table,这里不说了
        if (tab == null || (n = tab.length) == 0) {
            n = (sc > c) ? sc : c;
            if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if (table == tab) {
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = nt;
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
            }
        }
        else if (c <= sc || n >= MAXIMUM_CAPACITY)
            break;
            
        //主要看这里
        else if (tab == table) {
            //还是先计算扩容戳
            int rs = resizeStamp(n);
            if (sc < 0) { //表示已经是扩容状态了
                Node<K,V>[] nt;
                //和addCount()里的判断差不多,检查是不是善后状态是不是已经最大线程了什么的
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                    transferIndex <= 0)
                    break;
                    //sizeCtl低为加1,然后开始transfer()
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                    transfer(tab, nt);
            }
            //第一个扩容的线程
            else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                         (rs << RESIZE_STAMP_SHIFT) + 2))
                transfer(tab, null); //扩容
        }
    }
}

先未完待续吧,但也差不多了,东西实在是太多了