阅读 263

JDK1.8 ConcurrentHashMap源码解析

ConcurrentHashMap提高性能的关键点是多线程并发扩容.hash冲突会使得检索效率下降,所以扩容是一个减少冲突很好的方式.但是如果只有单线程进行迁移工作,其他读写线程都只能等待,效率就会比较低。concurrentHashMap同时存在2个成员变量table,nextTable.table存储当前数据,nextTable则是当发生扩容时搬运数据的目标容器.只要正在操作的slot没有与迁移的slot发生冲突,就可以最大限度提升扩容期间的并发读写。

下面解析一些核心的方法

public V get(Object key) {
    Node<K, V>[] tab;
    Node<K, V> e, p;
    int n, eh;
    K ek;
    // 生成 hash 值
    int h = spread(key.hashCode());
    // 定位到目标entry
    if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
        // 首先确定 hash 要相同 因为同一个slot 中 某些元素的hash可能不同
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        } else if (eh < 0)
	  // 从e 中寻找节点 负数代表为 TreeBin  或者正在移动中(MOVED)   (1)
            return (p = e.find(h, key)) != null ? p.val : null;
        // hash不匹配 往下遍历链表      
        while ((e = e.next) != null) {
            if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}
复制代码

可以观察到get方法是一个无锁方法,如果此时有其他线程修改链表会发生什么?
从put的实现中可以看到采用的是尾插法
假设新插入的entry是b,会挂载到a上.
此时刚好访问到a
2种情况:
1)read()a -> put()b -> read()a.next 这时能读取到最新的b节点
2)read()a -> read()a.next -> put()b 这时无法读取到最新的b节点
以上两种情况在高并发下都可以接受

可以看到(1)处hash值为负数,当发现entry.hash为负数时代表当前slot正在迁移中,稍后再解释,我们先梳理完不迁移的所有逻辑(树节点也是负数,但是有关树的查找不是本文要研究的点)

public boolean containsValue(Object value) {
    if (value == null)
        throw new NullPointerException();
    Node<K, V>[] t;
    if ((t = table) != null) {
        Traverser<K, V> it = new Traverser<K, V>(t, t.length, 0, t.length);
        for (Node<K, V> p; (p = it.advance()) != null; ) {
            V v;
            if ((v = p.val) == value || (v != null && value.equals(v)))
                return true;
        }
    }
    return false;
}
复制代码

Traverser该对象是ConcurrentHashMap的迭代器基类 从上面的代码描述大概可以知道使用方式,每当调用advance会返回一个node.


final Node<K, V> advance() {
    Node<K, V> e;
    // 当前已经找到了一个entry,直接返回next就可以
    if ((e = next) != null)
        e = e.next;
    for (; ; ) {
        Node<K, V>[] t;
        int i, n;  // must use locals in checks
        // 找到了e直接返回
        if (e != null)
            return next = e;
        // 此时已经超出了table的扫描范围 也就是遍历完了所有数据
        if (baseIndex >= baseLimit || (t = tab) == null ||
                (n = t.length) <= (i = index) || i < 0)
            return next = null;
        // hash < 0 代表此时entry是一个树结构 或者正在迁移中
        // tabAt(t, i),i = index 这两步已经根据最新的index定位到了entry
        if ((e = tabAt(t, i)) != null && e.hash < 0) {
            if (e instanceof ForwardingNode) {
                tab = ((ForwardingNode<K, V>) e).nextTable;
                e = null;
                pushState(t, i, n);
                continue;
            } else if (e instanceof TreeBin)
                e = ((TreeBin<K, V>) e).first;
            else
                e = null;
        }
        if (stack != null)
            recoverState(n);
        // slot下标+1
        else if ((index = i + baseSize) >= n)
            index = ++baseIndex; // visit upper slots if present
    }
}
复制代码

可以看到也是一个无锁方法,与get基本一致,树或者迁移逻辑先放一边.

下面来看一下数据是如何插入的

 final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    // 处理hash值 使得更加分散
    int hash = spread(key.hashCode());
    int binCount = 0;
// 注意这里还没有加锁 利用table的volatile修饰词 可以确保在table扩容完成后可以感知到变化
    for (Node<K, V>[] tab = table; ; ) {
        Node<K, V> f;
        int n, i, fh;
        // 如果容器还没有初始化 进行初始化
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
            // 初始化之后  如果对应slot 为null 直接设置
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // 尝试使用cas.直接设置成功就不需要后续处理 竞争失败可能是2种情况.刚好这个slot被标记成需要搬运数据,或者其他线程抢先插入了数据
            if (casTabAt(tab, i, null,
                    new Node<K, V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        // 发现在table上对应的slot正在发生数据迁移 协助一起搬运
        else if ((fh = f.hash) == MOVED)
            // 搬运的逻辑之后看  这是提升性能的关键
            tab = helpTransfer(tab, f);
        else {
            // 发生冲突 但是没有发生数据迁移 比较简单的情况
            V oldVal = null;
            // 因为要对链表进行操作,这里锁定头节点 (线程竞争的集中点)
            synchronized (f) {
                // double check
                // 2种情况发生冲突:
                // 1.当前节点正好在构建树
                // 2.数据迁移完成后更新table 当锁定该节点后,发现table本身已经改变了 这时要尝试重新定位槽,并再次锁定头节点
                // (从下面只有2个分支可以推断出 想要迁移某个slot 必然也是要获取头节点的锁 所以下面的处理再考虑hash = MOVED的情况了)
                if (tabAt(tab, i) == f) {
                    // 代表是链表结构
                    if (fh >= 0) {
                        // binCount 相当于记录了链表的长度 如果超过8 会转换成红黑树
                        binCount = 1;
                        for (Node<K, V> e = f; ; ++binCount) {
                            K ek;
                            // hashMap操作就不看了
                            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)
                    treeifyBin(tab, i);
                // 返回旧的值
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
// 这里主要是判断当前table内数据是否足够多,并进行扩容。
    addCount(1L, binCount);
    return null;
}
复制代码

链表的树化

private final void treeifyBin(Node<K, V>[] tab, int index) {
    Node<K, V> b;
    int n, sc;
    if (tab != null) {
        // 代表尝试树化时首先要保证 数组大小超过了 64 否则 选择扩容替代 树化
        if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
            // 进行扩容 默认变成2倍大小
            tryPresize(n << 1);
        else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
            // 继续锁定头节点  同样当发现table发生变化就是发生了扩容,选择放弃树化 因为扩容后同一个slot的深度变小了 就没有必要进行树化了
            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;
                    }
                    // 将头节点替换成一个特殊的node(TreeBin 代表本entry下是一个树结构) 初始化的同时还会进行树的平衡
                    setTabAt(tab, index, new TreeBin<K, V>(hd));
                }
            }
        }
    }
} 
复制代码

尝试进行扩容

private final void tryPresize(int size) {
    int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
            tableSizeFor(size + (size >>> 1) + 1);
    int sc;
    // 负数代表其他线程正在修改table
    while ((sc = sizeCtl) >= 0) {
        Node<K, V>[] tab = table;
        int n;
        // 如果数组还没有初始化 在这里逻辑 跟init 一样
        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) {
            // 此时n等同于table的原大小
            // 返回的是一个特殊的标记位 低14位代表当前table.size转换成二进制后左边有多少个0
            int rs = resizeStamp(n);
            // TODO 这个分支怎么进入的 进入while的前提应该是sc >= 0
            if (sc < 0) {
                Node<K, V>[] nt;
                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);
            }
            // 将SIZECTL修改成一个负数 同时包含一些特殊信息 比如原大小
            else if (U.compareAndSwapInt(this, SIZECTL, sc,
                    (rs << RESIZE_STAMP_SHIFT) + 2))
                transfer(tab, null);
        }
    }
}
复制代码

上面已经介绍了map的插入以及查询,是没有考虑到正在数据迁移的情况的,先看看数据迁移是怎么实现的

/**
 * Moves and/or copies the nodes in each bin to new table. See
 * above for explanation.
 *
 * @param tab     source容器
 * @param nextTab 目标容器  如果传入null 就代表要更新容器
 */
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

    // 代表需要更新nextTable容器  默认情况下扩容成2倍大小
    // 只有一条线程会以执行nextTable的更新 其余协助搬运的线程传入的nextTable不为空
    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 = nextTab;
        transferIndex = n;
    }
    int nextn = nextTab.length;
    // forwardingNode 就是标记当前slot正在进行数据迁移  这个节点可以在所有slot中共用
    ForwardingNode<K, V> fwd = new ForwardingNode<K, V>(nextTab);
    // 代表是否需要更新待搬运的slot范围
    boolean advance = true;
    boolean finishing = false; // to ensure sweep before committing nextTab
    // 到这里先缓一下 我们来推测一下接下来要做什么 想要完全理解concurrentHashMap快在哪要准确分析所有的线程冲突点
    // 旧的table通过检测ForwardingNode 发现slot需要扩容 并参与到扩容工作上,那么现在应该要将ForwardingNode逐步设置到旧table的所有slot中
    // 每个线程只负责搬运一部分slot 那么分界线是如何划分的 交界点的并发控制又是怎么做的(如果某个线程搬运完自己的部分后,发现还有剩余的slot肯定会继续搬运的)
    for (int i = 0, bound = 0; ; ) {
        Node<K, V> f;
        int fh;
        // 需要更新带搬运的slot范围
        while (advance) {
            // nextBound 代表本轮该线程搬运的slot对应的下标下限
            // nextIndex 代表搬运的slot对应的下标上限 也就是从哪里开始搬运
            int nextIndex, nextBound;
            // --i >= bound 代表上一个slot搬运完毕 往前走一格
            // finishing 代表已经完成数据扩容了 不需要更新slot范围 所以advance为false
            if (--i >= bound || finishing)
                advance = false;
            // 代表已经没有空闲的slot可以分配了
            else if ((nextIndex = transferIndex) <= 0) {
                i = -1;
                advance = false;
                // cas修改 transferIndex 就可以为多个线程并发搬运做分割了 每个成功修改的线程负责为对应的slot进行数据迁移
            } else if (U.compareAndSwapInt
                    (this, TRANSFERINDEX, nextIndex,
                            // 如果此时待搬运的块 超过了每个线程分工的slot数量 减少对应的部分 如果不足一个stride 剩余的都由该线程搬运
                            nextBound = (nextIndex > stride ?
                                    nextIndex - stride : 0))) {
                // 确定好了要搬运的范围  也就是 nextBound~nextIndex
                bound = nextBound;
                i = nextIndex - 1;
                advance = false;
            }
        }

        // 此时本次范围内要搬运的所有slot都已经搬运完成
        if (i < 0 || i >= n || i + n >= nextn) {
            int sc;
            // 本次所有数据搬运完成 扩容结束
            if (finishing) {
                nextTable = null;
                table = nextTab;
                sizeCtl = (n << 1) - (n >>> 1);
                return;
            }
            // sc此时携带了参与数据迁移的线程数的信息 这里代表减少一条线程
            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
            }
            // 这是最简单的情况 当前要搬运的slot下没有任何entry 但是要设置一个占位符 这样其他线程就会协助扩容
            // 修改成功返回true 代表本slot不需要再搬运了 更新i
        } else if ((f = tabAt(tab, i)) == null)
            advance = casTabAt(tab, i, null, fwd);
            // 发现当前slot已经由其他线程负责搬运了 更新i
            // 不过老实说 既然已经cas更新transferIndex了 线程间应该不会在搬运过程中发生slot的冲突 但是不影响理解 只要继续搬运下一个slot就可以
        else if ((fh = f.hash) == MOVED)
            advance = true; // already processed
        // 最普遍的情况 该slot下有数据需要搬运
        else {
            // 这里也锁定了header节点 就与putValue 以及树化产生了锁竞争 顺便再声明一次当某个slot尝试树化时发现已经在搬运数据了 就会停止树化
            // 但是当竞争到锁的时候头节点可能已经变成了一个TreeBinNode
            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;
                            // 代表出现了某个节点hash跟上个节点hash不一致的情况
                            if (b != runBit) {
                                runBit = b;
                                // lastRun 就是上个hash不同的节点
                                lastRun = p;
                            }
                        }
                        // 根据 hash & n 的结果划分不同的情况
                        // 注意 ln(hn) 后面可能还有一些节点
                        if (runBit == 0) {
                            ln = lastRun;
                            hn = null;
                        } else {
                            hn = lastRun;
                            ln = null;
                        }
                        // 只需要遍历到lastRun之前的节点 因为后面的链表还没有打散
                        for (Node<K, V> p = f; p != lastRun; p = p.next) {
                            int ph = p.hash;
                            K pk = p.key;
                            V pv = p.val;
                            // 根据hash是否刚好是n的倍数 拆分成2个小链表
                            if ((ph & n) == 0)
                                ln = new Node<K, V>(ph, pk, pv, ln);
                            else
                                hn = new Node<K, V>(ph, pk, pv, hn);
                        }
                        // 将2个小链表插入到新的table中
                        setTabAt(nextTab, i, ln);
                        setTabAt(nextTab, i + n, hn);
                        // 因为整个扩容动作还没有完成 将该slot标记成扩容中 这样新的put线程发现slot对应的标记就会协助扩容
                        // 这里有一个隐含的好处 就是在扩容中,如果旧的table对应的slot又插入了新的节点,之前针对该slot的移动就白费了.
                        // 梳理下来是这种情况 当整个table触发扩容时,往某个还未开始转移的slot插入数据是可行的,但是一旦当转移的线程抢占到这个slot后,打上标记,并在整个扩容完成前都不允许再操作这个slot了
                        // 而hash冲突的线程也没有让它干等着 而是再看到move标记后协助数据迁移
                        setTabAt(tab, i, fwd);
                        // 继续转移下个slot
                        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;
                    }
                }
            }
        }
    }
}
复制代码

从上面的代码可以总结出,每当有一条线程参与数据迁移,会修改SIZECTL的值, 当所有数据完成迁移后,使用新table替换旧table,因为table使用volatile修饰,所以能观测到变化.其他线程在帮助数据迁移完成后会不断的自旋,等待table更新

来看看其他线程发现MOVE标记时如何处理

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) {
        int rs = resizeStamp(tab.length);
        while (nextTab == nextTable && table == tab &&
                // 当发生扩容时,sizeCtl是一个特殊的负数,而初始化table时对应-1
                (sc = sizeCtl) < 0) {
            // 代表此时已经没有slot可分配给该线程了 
            if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || transferIndex <= 0)
                break;
            // 代表此时正在迁移数据的线程数+1
            if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                transfer(tab, nextTab);
                break;
            }
        }
        return nextTab;
    }
    return table;
}
复制代码

在完成了最重要的数据迁移逻辑的解析后,其他一些方法的实现大同小异,先通过hash值找到目标slot,如果发现被标记为MOVE,就参与数据迁移.如果是正常节点就锁定头节点.之后就是一些正常的插入,替换,删除操作.

现在回过头来看get()请求在发现hash为负数时怎么处理 入口在entry.find()方法 (发现TreeNode的find方法有一个检查锁的步骤,原因是红黑树在平衡时需要加锁. 这里将通过cas修改volatile int lockState 属性实现并发控制.不展开了)

那么如果此时get对应的slot正好在进行数据迁移该怎么处理

Node<K, V> find(int h, Object k) {
    // loop to avoid arbitrarily deep recursion on forwarding nodes
    outer:
    for (Node<K, V>[] tab = nextTable; ; ) {
        Node<K, V> e;
        int n;
        if (k == null || tab == null || (n = tab.length) == 0 ||
                (e = tabAt(tab, (n - 1) & h)) == null)
            return null;
        for (; ; ) {
            int eh;
            K ek;
            if ((eh = e.hash) == h &&
                    ((ek = e.key) == k || (ek != null && k.equals(ek))))
                return e;
            if (eh < 0) {
		// 数据迁移是不会发生嵌套的,这种情况不可能发生.
                if (e instanceof ForwardingNode) {
                    tab = ((ForwardingNode<K, V>) e).nextTable;
                    continue outer;
                    // 从树节点中读取
                } else
                    return e.find(h, k);
            }
            if ((e = e.next) == null)
                return null;
        }
    }
}
复制代码

slot头节点的操作顺序在扩容时的操作顺序是,先锁定头节点,完成数据迁移,修改成MOVE标识.所以当调用get并发现MOVE标记时,代表有关这个slot的数据迁移已经完成,直接去扩容的目标容器查询数据就可以了. 不过这里有一个问题:迁移完成的瞬间nextTable会被置空,这个时候如果刚好触发find不就没办法获取到数据了吗?

下面回到这个方法 看一下当发现某个entry正在迁移时会怎么处理

final Node<K, V> advance() {
    Node<K, V> e;
    // next != null 代表当前entry是一个链表结构每次只要返回下一个节点就可以
    if ((e = next) != null)
        e = e.next;
    for (; ; ) {
        Node<K, V>[] t;
        int i, n;  // must use locals in checks
        // 找到了e直接返回
        if (e != null)
            return next = e;
        // 此时已经超出了table的扫描范围 也就是遍历完了所有数据
        if (baseIndex >= baseLimit || (t = tab) == null ||
                (n = t.length) <= (i = index) || i < 0)
            return next = null;
        // hash < 0 代表此时entry是一个树结构 或者正在迁移中
        // tabAt(t, i),i = index 这两步已经根据最新的index定位到了entry
        if ((e = tabAt(t, i)) != null && e.hash < 0) {
            if (e instanceof ForwardingNode) {
                tab = ((ForwardingNode<K, V>) e).nextTable;
                e = null;
// 将之前的table,index,table.length暂时存起来
                pushState(t, i, n);
                continue;
            } else if (e instanceof TreeBin)
                e = ((TreeBin<K, V>) e).first;
            else
                e = null;
        }
        if (stack != null)
// 利用stack还原之前存储的属性
            recoverState(n);
            // 正常的逻辑就是每次下标+1
        else if ((index = i + baseSize) >= n)
            index = ++baseIndex; // visit upper slots if present
    }
}
复制代码

当发现当前entry是一个迁移中的entry,先将此时的table,index,n存储到一个stack结构中.并且因为tab得到了更新,下一轮t发生了变化,就可以读取到index在迁移数组所对应的slot了.此时通过 e=tabAt(t,i)获取到entry后.因为stack被赋值.执行recoveryState还原了table以及相关指针.然后再下一次循环中利用 if (e != null) return next = e; 遍历迁移后的数据.

看看pushState/recoveryState分别做了什么

private void pushState(Node<K, V>[] t, int i, int n) {
    // 当pushState 在执行recoveryState前发生冲入时 会更新spare 但是一次get()请求只对应一个Traverser对象应该是不会重入的
    TableStack<K, V> s = spare;  // reuse if possible
    if (s != null)
        spare = s.next;
    else
        s = new TableStack<K, V>();
    // 将当前标识信息暂存到这个结构中
    s.tab = t;
    s.length = n;
    s.index = i;
    s.next = stack;
    stack = s;
}
private void recoverState(int n) {
    TableStack<K, V> s;
    int len;
    while ((s = stack) != null && (index += (len = s.length)) >= n) {
        n = len;
        // 将stack的数据还原到当前对象
        index = s.index;
        tab = s.tab;
        s.tab = null;
        TableStack<K, V> next = s.next;
        s.next = spare; // save for reuse
        stack = next;
        spare = s;
    }
// 最终还是进入这个分支,只是将index加1
    if (s == null && (index += baseSize) >= n)
        index = ++baseIndex;
}
复制代码

其他遍历方法都是以这个类作为模板.


文章分类
后端
文章标签