ConcurrentHashMap 源码全解一

ConcurrentHashMap 源码全解一

该篇主要讲解了数据插入操作流程以及数组扩容流程,插入过程通过 synchronized 和 CAS 保证多线程安全

核心成员及变量

transient volatile Node<K,V>[] table; // 保存数据的数组,容量总是 2 的 N 次方(由tableSizeFor方法控制)
private transient volatile Node<K,V>[] nextTable;// 该数组用于扩容时使用
// 下述 3 个变量的作用:在对 hash 表中元素计数加 1 的时候,若没有多线程竞争直接通过 baseCount 进行增加,若出现多线程竞争,将累加操作分散到 CounterCell 数组中进行,cellsBusy 用于控制 CounterCell 数组项是否也存在竞争。具体原理请看【[JUC-LongAdder](https://blog.csdn.net/qq_24931785/article/details/129429941)】一篇的讲解
private transient volatile long baseCount;
private transient volatile int cellsBusy;
private transient volatile CounterCell[] counterCells;

private transient volatile int sizeCtl;// 用于控制数组初始化于扩容操作
private transient volatile int transferIndex;// 在扩容时通过改变量对每个线程负责转移元素的范围
private static final int MAXIMUM_CAPACITY = 1 << 30;// hash 表最大容量
private static final int DEFAULT_CAPACITY = 16; // 默认容量
static final int TREEIFY_THRESHOLD = 8; // 链表转为红黑树的阈值,前提是数组长度大于 MIN_TREEIFY_CAPACITY
static final int UNTREEIFY_THRESHOLD = 6;// 红黑树转为链表的阈值
static final int MIN_TREEIFY_CAPACITY = 64; // 控制链表转为红黑树的前提
private static final int MIN_TRANSFER_STRIDE = 16;// 扩容时每个线程默认帮助数据转移的个数
// 下述三个变量在数组扩容时使用,低 16 位控制扩容时最大参与线程数
private static int RESIZE_STAMP_BITS = 16;
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;

static final int MOVED     = -1; // 标识节点已被转移
static final int TREEBIN   = -2; // 标识节点是红黑树节点
static final int RESERVED  = -3; // computeIfAbsent 方法中使用,创建ReservationNode节点的 hash 值
static final int HASH_BITS = 0x7fffffff; // 计算 hash 值时用
// 保存 key-value 的节点类
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;// key 的hash 值
    final K key;
    volatile V val;
    volatile Node<K,V> next;// 发生 hash 冲突时链接到下一个节点
}
// 占位符节点,扩容时数组元素转移成功时的填充节点
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;
    }
}
// 红黑树的树节点
static final class TreeNode<K,V> extends Node<K,V> {
    TreeNode<K,V> parent;  // red-black tree links
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // needed to unlink next upon deletion
    boolean red;

    TreeNode(int hash, K key, V val, Node<K,V> next,
             TreeNode<K,V> parent) {
        super(hash, key, val, next);
        this.parent = parent;
    }
}
// 链表转为红黑树时,放在 数组项中的头结点
static final class TreeBin<K,V> extends Node<K,V> {
    TreeNode<K,V> root; // 根节点
    volatile TreeNode<K,V> first;// 链表的第一个节点
    volatile Thread waiter; // 等待线程
    volatile int lockState; //锁状态
    // values for lockState
    static final int WRITER = 1; // 写锁
    static final int WAITER = 2; // 等待写锁
    static final int READER = 4; // 读锁
}

put 添加方法

1、计算 hash 值,用于计算数组下标

2、for 循环保证插入成功

3、数组未初始化,进行初始化

4、通过 hash 算出的下标处没有值时,封装Node节点 CAS 放入该下标处

5、hash 冲突时,通过 synchronized 对该节点元素上锁,封装Node节点插入链表或红黑树,插入时如果存在相同的 key 则 根据 onlyIfAbsent 判断是否覆盖旧值

6、增加hash表元素计数

public V put(K key, V value) {
    return putVal(key, value, false);
}
// onlyIfAbsent 是否替换旧值
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode()); // 计算 hash 值
    int binCount = 0; // 记录同一个数组槽(slot)中的元素个数,也即链表中的元素个数
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            tab = initTable(); // 初始化 hash 表(存放数据的 Node 数组)
        // tabAt方法 U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE) 通过偏移量的方式获取对应下标处的值
        // 数组基地址 + 偏移量(数组下标 * 数组项的宽度(也即数组项类型的大小,如 int 类型,其大小为 4 byte))
        // Unsafe 类的 xxxVolatile方法 使当前数组项具有 volatile 的语义
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
                break;                   
        }
        // 执行到这一步,意味着 hash 表正在进行扩容,通过 MOVED 来标识,如果正在迁移,那么调用 helpTransfer 帮助进行迁移,算法实现同下面的 transfer 方法
        else if ((fh = f.hash) == MOVED) 
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            synchronized (f) { // f 不为 null 代表发生了hash 冲突,那么对 f 进行上锁(链表的头结点上锁,避免其它线程的并发操作)
                if (tabAt(tab, i) == f) {// 当前 数组下标处的元素未发生变更
                    if (fh >= 0) { // 正常链表节点
                        binCount = 1; // 数组槽中已经存在一个节点 f ,所以 binCount 为 1
                        for (Node<K,V> e = f;; ++binCount) { // 循环插入链表 
                            K ek;
                            // 表明链表中存在相同 key ,根据 onlyIfAbsent 判断替换原值
                            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) { // 创建 Node 节点链在链表尾部
                                pred.next = new Node<K,V>(hash, key, value, null);
                                break;
                            }
                        }
                    }
                    else if (f instanceof TreeBin) {// 链表已经转换为红黑树
                        Node<K,V> p;
                        binCount = 2; // 红黑树不可能只有一个根节点,因为会在 数组项中放置一个 TreeBin 对象,然后才是根节点,所以设置 binCount = 2
                        // 将节点插入红黑树,此处不对红黑树结构做过多讲解,详细算法实现将在后续篇章讲解,刚兴趣的同学可以先自行研究
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {// 返回不为空表明红黑树已经存在 相同 key 的节点,根据 onlyIfAbsent 判断是否替换旧值
                            oldVal = p.val;
                            if (!onlyIfAbsent) p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD) // 节点个数 超过阈值 8 ,尝试转换为红黑树。稍后讲解该方法具体实现
                    treeifyBin(tab, i);
                if (oldVal != null) return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);// 增加节点个数
    return null;
}

initTable hash表初始化

通过 sizeCtl 控制 hash 表的初始化过程,只有一个线程对其进行初始化,初始化完成后保存 sizeCtl = hash 表容量的 0.75 倍,用于后续控制 hash 表的扩容

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)) { // 将 sizeCtl  设置为 -1 成功,进行 hash 表的初始化工作
            try {
                if ((tab = table) == null || tab.length == 0) {
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    sc = n - (n >>> 2); // sc 保存 hash 表中存放元素的最大值,达到该值时需要进行扩容(默认为 hash 表容量的 0.75 倍,也即 tab.length * 0.75,例如 tab.length = 16,那么 sizeCtl = sc = 12)
                }
            } finally {
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

addCount 增加计数

增加hash表元素计数,根据 check 判断是否需要对 hash 表进行扩容,扩容时通过 rs 控制参与扩容的线程数

// addCount(1L, binCount);
private final void addCount(long x, int check) {
    CounterCell[] as; long b, s;
    // 增加 counter 的算法同 LongAdder 的实现原理一样,具体请看【JUC-LongAdder】一篇的讲解
    if ((as = counterCells) != null ||
        !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
        // 省略代码 ...
        s = sumCount();// 算出 hash 表中元素个数
    }
    
    if (check >= 0) { // 检查 hash 表是否需要扩容
        Node<K,V>[] tab, nt; int n, sc;
        // hash 表元素个数 大于 hash 表的扩容阈值 并且 hash 表长度为达到最大限制范围
        while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
               (n = tab.length) < MAXIMUM_CAPACITY) {
            // resizeStamp(n) 计算扩容时使用的 stamp 值,将其左移到高 16 位,因为低 16 位用于表示 帮助扩容的线程数
            int rs = resizeStamp(n) << RESIZE_STAMP_SHIFT; 
            // sizeCtl < 0 表示 hash 表正在扩容(此时 sizeCtl 值已被设置为 rs + 2 的值),那么该线程将帮助其完成扩容动作
            if (sc < 0) { 
                // MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1  最大帮助扩容线程个数(默认低 16 位) 
                if (sc == rs + MAX_RESIZERS || sc == rs + 1 // 表示已达到最大帮助线程个数
                    || (nt = nextTable) == null || transferIndex <= 0) // 扩容已完成
                    break;
                // CAS 增加帮助线程个数,CAS 成功 调用 transfer 帮助扩容
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) 
                    transfer(tab, nt);
            }
            // 将 sizeCtl 设置为 rs + 2,设置成功,开始扩容
            else if (U.compareAndSwapInt(this, SIZECTL, sc, rs + 2))
                transfer(tab, null);
            s = sumCount(); // 获取最新元素个数
        }
    }
}

treeifyBin 转换红黑树

将链表转换为红黑树

private final void treeifyBin(Node<K,V>[] tab, int index) {
    Node<K,V> b; int n, sc;
    if (tab != null) {
        // hash 表元素个数 小于 MIN_TREEIFY_CAPACITY = 64 时,尝试扩容 hash 表
        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)); // 在 TreeBin 构造函数中完成红黑树构建
                }
            }
        }
    }
}

tryPresize 扩容

扩容时通过计算一个 stamp 值来记录标记当前扩容周期,resizeStamp 方发原理:numberOfLeadingZeros方法算出 n 的二进制前面有几个 0 ,或 上 (1 << 15) 保证整型的 低16位 的高位 为 1 ,当下面执行 rs << RESIZE_STAMP_SHIFT 时 整型的高位变为 1 ,即为负数。只要 sc 为负数,就表示正在进行扩容

private final void tryPresize(int size) {
    int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
        tableSizeFor(size + (size >>> 1) + 1); // 计算新数组大小,原数组的 2 倍
    int sc;
    while ((sc = sizeCtl) >= 0) {
        Node<K,V>[] tab = table; int n;
        if (tab == null || (n = tab.length) == 0) {
            // 此处为数组初始化流程 请看 initTable 方法,此时我们关注下面的扩容操作
        }
        else if (c <= sc // 容量 小于 扩容阈值,无需扩容
                 || n >= MAXIMUM_CAPACITY) break; // 原容量以达到最大值,无需扩容
        else if (tab == table) { // 原 hash 表为发生更改,进行扩容
            // resizeStamp 方法 Integer.numberOfLeadingZeros(n) | (1 << 15),numberOfLeadingZeros方法算出 n 的二进制前面有几个 0 ,或 上 (1 << 15) 保证整型的 低16位 的高位 为 1 ,当下面执行 rs << RESIZE_STAMP_SHIFT 时 整型的高位变为 1 ,即为负数
            int rs = resizeStamp(n);// 计算扩容时使用的 stamp 值
            // sizeCtl < 0 表示 hash 表正在扩容(此时 sizeCtl 值已被设置为 rs + 2 的值),那么该线程将帮助其完成扩容动作
            // 下述逻辑同上述 addCount 方法,详情请看 addCount 方法
            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 设置为 (rs << RESIZE_STAMP_SHIFT) + 2 此值为负数,设置成功,开始扩容
            else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2))
                transfer(tab, null);
        }
    }
}

transfer 转移数据

1、计算每个线程负责转移数据的范围值大小 stride,用于将原始数组切割为多个区间,每个线程负责其中一个区间

2、转移数组未初始化,进行初始化

3、创建一个ForwardingNode(该节点的 hash 值等于 -1)节点,用于填充原始数组中已被转移数组项(转移到新数组后将原数组的数据替换为 ForwardingNode 节点,标识数据已被转移,也即数组正在进行扩容,当其它线程发现节点是 ForwardingNode 节点时,将会参与到数组扩容转移数据的操作中),转移过程中如果发现数组项为空,也将其填充 ForwardingNode 节点

4、通过第一步计算的 stride 值计算出该线程所负责转移的区间,通过 i 和 bound 标识;另外 通过 advance 来标识是否需要向前继续查找需要转移的数据,通过 finishing 标识转移是否完成,只有当所有参与线程都完成各自的转移操作 finishing 才会被设置为 true

5、如果当前转移节点是 列表结构,遍历链表元素通过 节点的 hash 值 & 上 原数组长度 (fh & n),根据返回结果是否为 0 将原链表拆分为 ln 和 hn 两个链表,ln 链表在新数组中的位置保持为原始下标处,hn 链表将会保存在 原始下标 + 原始数组长度 的 下标处

6、如果当前转移节点是 红黑树节点,原理同第五步,将红黑树转为 ln 和 hn 的两个链表(此时是双向链表结构),然后再根据每个链表中的元素个数决定是否转为红黑树保存

读者可能对 ln 和 hn 两个链表结构的划分有疑虑,在此我已一个简单的示例来说明一下,希望对大家理解源码有所帮助

如 数组长度 length = 8,最大下标为7,扩容后 length = 16,最大索引下标为 15

8 >>>> 0000 1000 7 >>>> 0000 0111 16 >>>> 0001 0000 15 >>>> 0000 1111

加入数据保存在 index = 4 的位置,那么 该数据的hash值二进制的后3位 一定是 100,其他位是啥不会影响到下标的计算 如

hash1 = 0101 1100 ,hash1 & 0000 0111 = 0000 0100 = 4

hash2 = 1011 0100 ,hash2 & 0000 0111 = 0000 0100 = 4

hash1 & 0000 1000 = 0000 1000 hash1 & 0000 1111 = 0000 1100 = 12 = 4 + 8 (原index + 原length)

hash2 & 0000 1000 = 0000 0000 hash2 & 0000 1111 = 0000 0100 = 4 (原index)

由此可见当 hash 值与上 原length 的值等于 原length 时,此数据将被分散到新数组的 (原 index + 原length)处,

hash 值与上 原length 的值等于 0 时,此数据将被保存在新数组对应的index 处

这就是打散 ln 和 hn 的原理

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
    if (nextTab == null) {   // 创建新数组    
        try {
            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
            nextTab = nt;
        } catch (Throwable ex) {}
        nextTable = nextTab;
        transferIndex = n;// 记录转移元素开始索引位置,初始为 原数组长度
    }
    int nextn = nextTab.length;
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab); // 创建一个转移节点
    boolean advance = true; // 标志是否继续向前寻找转移节点
    boolean finishing = false; // 标识转移是否完成
    // i(高位) 标识开始转移元素的下标,bound(低位) 标识该线程负责转移的范围,转移顺序是从数组尾部开始先前进行
    for (int i = 0, bound = 0;;) { 
        Node<K,V> f; int fh;
        while (advance) {
            int nextIndex, nextBound;
            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;
                i = nextIndex - 1;
                advance = false;
            }
        }
        if (i < 0 || i >= n || i + n >= nextn) {
            int sc;
            if (finishing) { // 转移完成
                nextTable = null;
                table = nextTab; // 将 新数组 赋值给 table
                sizeCtl = (n << 1) - (n >>> 1);// 保存下一次需要扩容的阈值
                return;
            }
            // 帮助转移线程数 减 1 (进入transfer方法前加了 1)
            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                // 两者不等表示当前线程不是最后一个完成的线程,所以直接退出即可;当是最后一个线程时,设置 finishing = ture 标识扩容完成
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    return;
                finishing = advance = true;
                i = n; // recheck before commit
            }
        }
        else if ((f = tabAt(tab, i)) == null) // 如果当前下标处没有存放元素,那么将改下标出存入 ForwardingNode 节点,也用于标识当前正在对 hash 表进行扩容
            advance = casTabAt(tab, i, null, fwd);
        else if ((fh = f.hash) == MOVED)// 表示当前节点已被转移
            advance = true; // already processed
        else {
            synchronized (f) {// 对头结点进行上锁,避免在转移期间其它线程进行插入操作
                if (tabAt(tab, i) == f) {// 节点未发生改变
                    Node<K,V> ln, hn; // ln 表示 低位节点;hn 表示 高位节点
                    if (fh >= 0) {// 表示当前是链表节点
                        // runBit == 0 表示当前节点为 ln (在新数组中保存的下标等于原始数组的下标),否则为 hn (保存在新的下标处)
                        // 例如原始数组长度是 16,扩容后长度是 32(切分为 高 16 位和 低 16 位),ln 节点将继续保存在 低16位中,hn 将被散列到 高 16 位中
                        int runBit = fh & n; 
                        Node<K,V> lastRun = f; // lastRun 默认为 头结点
                        // 遍历找到最后一个节点,lastRun 指向最后一个节点 p,ln 和 hn 取决于 runBit 位是否为 0
                        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;
                        }
                        // 从头结点遍历,知道lastRun节点,此循环的目的就是为了将原先的一个链表拆分为 ln 和 hn 两个链表
                        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); // 将 ln 链表存入 新数组 原来的 index 处
                        setTabAt(nextTab, i + n, hn);// hn 将被散列到(原 index + 原数组长度) 的下标处
                        setTabAt(tab, i, fwd);// 将原数组的 i 下标处设置为 ForwardingNode 节点,标识节点已转移
                        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 循环原理同 上述链表节点的原理一样,将原先的红黑树切分为两个 链表(双向链表)
                        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;
                            }
                        }
                        // 当 lc 计数小于等于 6 的时候,调用untreeify方法转为普通链表保存,当 hc 计数等于 0 时,直接将原先的红黑树节点赋值给 ln 即可,否则将 lo 转为新的红黑树保存;hn 处理方式也一样
                        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);// 将 ln 链表存入 新数组 原来的 index 处
                        setTabAt(nextTab, i + n, hn);// hn 将被散列到(原 index + 原数组长度) 的下标处
                        setTabAt(tab, i, fwd);// 将原数组的 i 下标处设置为 ForwardingNode 节点,标识节点已转移
                        advance = true;// 向前递进查找需要转移的节点
                    }
                }
            }
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值