Java8 ConcurrentSkipListMap与ConcurrentSkipListSet (二) 源码解析

    目录

1、compute / merge

2、higher / lower /ceiling / floor

3、first / last 

4、pollFirstEntry / pollLastEntry

5、keySet / descendingKeySet / Iter

6、tailMap / headMap / subMap /SubMap


     本篇博客继续上一篇《Java8 ConcurrentSkipListMap与ConcurrentSkipListSet (一) 源码解析》讲解ConcurrentNavigableMap相关方法的实现细节。

1、compute / merge

      compute一共有三个方法,compute、computeIfAbsent和computeIfPresent,这四个方法的实现类似,都是先找到目标key,然后用remappingFunction计算结果,方法含义跟ConcurrentHashMap中一样的,不再赘述。


public V compute(K key,
                     BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
        if (key == null || remappingFunction == null)
            throw new NullPointerException();
        for (;;) {
            Node<K,V> n; Object v; V r;
            if ((n = findNode(key)) == null) { //没有找到目标节点
                if ((r = remappingFunction.apply(key, null)) == null) //计算返回null,跳出循环返回null
                    break;
                if (doPut(key, r, true) == null) //r不为null,插入新节点,doPut返回值不是null,则重新for循环,进入下面的else if分支
                    return r;
            }
            else if ((v = n.value) != null) { //找到目标节点,如果value为null,下一次循环findNode会将该节点删除
                @SuppressWarnings("unchecked") V vv = (V) v;
                if ((r = remappingFunction.apply(key, vv)) != null) { //r不为null,修改value
                    if (n.casValue(vv, r)) //cas失败,则再次for循环
                        return r;
                }
                else if (doRemove(key, vv) != null) //r为null,删除节点
                    break;
            }
        }
        return null;
    }

public V computeIfAbsent(K key,
                             Function<? super K, ? extends V> mappingFunction) {
        if (key == null || mappingFunction == null)
            throw new NullPointerException();
        V v, p, r;
        if ((v = doGet(key)) == null &&   //目标key不存在
            (r = mappingFunction.apply(key)) != null) //计算结果非空
            v = (p = doPut(key, r, true)) == null ? r : p;
        return v;
    }

  
public V computeIfPresent(K key,
                              BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
        if (key == null || remappingFunction == null)
            throw new NullPointerException();
        Node<K,V> n; Object v;
        while ((n = findNode(key)) != null) { //目标key 存在且value非空
            if ((v = n.value) != null) {
                @SuppressWarnings("unchecked") V vv = (V) v;
                V r = remappingFunction.apply(key, vv);
                if (r != null) { //r不为null,修改value
                    if (n.casValue(vv, r))
                        return r;
                }
                else if (doRemove(key, vv) != null) //r为null,删除节点
                    break;
            }
        }
        return null;
    }

    
public V merge(K key, V value,
                   BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
        if (key == null || value == null || remappingFunction == null)
            throw new NullPointerException();
        for (;;) {
            Node<K,V> n; Object v; V r;
            if ((n = findNode(key)) == null) { //没有匹配的key,插入新节点
                if (doPut(key, value, true) == null)
                    return value;
            }
            else if ((v = n.value) != null) { //找到目标key
                @SuppressWarnings("unchecked") V vv = (V) v;
                if ((r = remappingFunction.apply(vv, value)) != null) { //r不为null,修改value
                    if (n.casValue(vv, r)) 
                        return r;
                }
                else if (doRemove(key, vv) != null) //r为null,删除节点
                    return null;
            }
        }
    }

2、higher / lower /ceiling / floor

    这四个,每个都有两个方法,如ceilingKey和ceilingEntry,前者返回Key,后者返回一个Map.Entry键值对,总共8个方法;其中higher和lower相对,lower表示获取小于key的最大节点,higher表示获取大于key的最小节点;ceiling 与 floor相对,floor表示获取小于或者等于key的最大节点,ceiling 表示获取大于或者等于key的最小节点,其实现如下:

public Map.Entry<K,V> lowerEntry(K key) {
        //找到小于key的最大节点
        return getNear(key, LT);
    }

public K lowerKey(K key) {
        Node<K,V> n = findNear(key, LT, comparator);
        return (n == null) ? null : n.key;
    }

public Map.Entry<K,V> higherEntry(K key) {
       //找到大于key的最小节点
        return getNear(key, GT);
    }

    
public K higherKey(K key) {
        Node<K,V> n = findNear(key, GT, comparator);
        return (n == null) ? null : n.key;
    }

public Map.Entry<K,V> floorEntry(K key) {
        //找到小于或者等于key的最大节点
        return getNear(key, LT|EQ);
    }

public K floorKey(K key) {
        Node<K,V> n = findNear(key, LT|EQ, comparator);
        return (n == null) ? null : n.key;
    }

   
public Map.Entry<K,V> ceilingEntry(K key) {
         //找到大于或者等于key的最小节点
        return getNear(key, GT|EQ);
    }

    
public K ceilingKey(K key) {
        Node<K,V> n = findNear(key, GT|EQ, comparator);
        return (n == null) ? null : n.key;
    }

//rel是一个常量
final Node<K,V> findNear(K key, int rel, Comparator<? super K> cmp) {
        if (key == null)
            throw new NullPointerException();
        for (;;) {
            //找到最底层中接近key且小于key的节点b,然后往后遍历
            for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
                Object v;
                if (n == null) //遍历万彩城
                    return ((rel & LT) == 0   //rel的操作就是GT,大于 
                                  || b.isBaseHeader()) ? null : b;
                Node<K,V> f = n.next;
                if (n != b.next)     //next节点变了,重新读取b
                    break;
                if ((v = n.value) == null) {      //n被删除了
                    n.helpDelete(b, f);
                    break;
                }
                if (b.value == null || v == n)      //b被删除了
                    break;
                int c = cpr(cmp, key, n.key);
                if ((c == 0 && (rel & EQ) != 0) || //rel中包括EQ等于,即EQ表示找到key值相等的节点
                    (c <  0 && (rel & LT) == 0))  // rel中包括EQ或者GT,即GT|EQ表示大于等于key的最小节点
                    return n;
                if ( c <= 0 && (rel & LT) != 0) //rel中包括LT,即LT表示小于等于key的最大节点
                    return b.isBaseHeader() ? null : b;
                b = n;
                n = f;
            }
        }
    }

    
final AbstractMap.SimpleImmutableEntry<K,V> getNear(K key, int rel) {
        Comparator<? super K> cmp = comparator;
        for (;;) {
            Node<K,V> n = findNear(key, rel, cmp);
            if (n == null)
                return null;
            //根据key和value创建一个键值对    
            AbstractMap.SimpleImmutableEntry<K,V> e = n.createSnapshot();
            if (e != null)
                return e;
        }
    }

3、first / last 

      每个也是有两个方法,firstKey和firstEntry,lastKey和lastEntry,底层实现分别基于findFirst和findLast,其中findFirst很简单,head.node.next就是第一个节点,findLast比较麻烦,从head开始先往右遍历到最后然后进入下一个层级继续往右遍历,一直遍历到最底层Node节点的最右边的节点,这期间如果发现某个节点被删除了,还需要重新读取head,从头遍历一遍。其实现如下:

 public K firstKey() {
        //获取第一个节点
        Node<K,V> n = findFirst();
        if (n == null)
            throw new NoSuchElementException();
        return n.key;
    }

public Map.Entry<K,V> firstEntry() {
        for (;;) {
            Node<K,V> n = findFirst();
            if (n == null)
                return null;
            AbstractMap.SimpleImmutableEntry<K,V> e = n.createSnapshot();
            if (e != null)
                return e;
        }
    }
    
public K lastKey() {
        //获取最后一个节点
        Node<K,V> n = findLast();
        if (n == null)
            throw new NoSuchElementException();
        return n.key;
    }
   
public Map.Entry<K,V> lastEntry() {
        for (;;) {
            Node<K,V> n = findLast();
            if (n == null)
                return null;
            AbstractMap.SimpleImmutableEntry<K,V> e = n.createSnapshot();
            if (e != null)
                return e;
        }
    }

final Node<K,V> findFirst() {
        for (Node<K,V> b, n;;) {
            if ((n = (b = head.node).next) == null) //head.node.next为null,空链表,返回null
                return null;
            if (n.value != null) 
                return n;
            //n.value为null,删除该节点    
            n.helpDelete(b, n.next);
        }
    }

final Node<K,V> findLast() {
        Index<K,V> q = head;
        for (;;) {
            Index<K,V> d, r;
            if ((r = q.right) != null) { //right节点不为空,则沿着right属性一直向右遍历
                if (r.indexesDeletedNode()) {//如果所引用的节点被删除了,将其从right链表中移除 
                    q.unlink(r);
                    q = head; //重新读取head,从头开始,因为删除节点后可能导致层级变了
                }
                else
                    q = r;
            } else if ((d = q.down) != null) { //right节点为空,进入下一个层级,继续往右遍历
                q = d;
            } else {
                //down节点为空,遍历到最底层的Node节点了,通过next一直往后遍历到最后
                for (Node<K,V> b = q.node, n = b.next;;) {
                    if (n == null)
                        return b.isBaseHeader() ? null : b;
                    Node<K,V> f = n.next;            
                    if (n != b.next)  //b的next节点变了,重读读取b
                        break;
                    Object v = n.value;
                    if (v == null) {                 // n被删除了
                        n.helpDelete(b, f);
                        break;
                    }
                    if (b.value == null || v == n)      //b被删除了
                        break;
                    b = n;
                    n = f;
                }
                q = head; //上面因为节点被删除后终止for循环,会进入此代码,重新读取head
            }
        }
    }

4、pollFirstEntry / pollLastEntry

      这两个方法分别用于移除并返回第一个节点和最后一个节点,其查找第一个节点和最后一个节点的逻辑和上面的findFirst和findLast是基本一致的。

public Map.Entry<K,V> pollFirstEntry() {
        return doRemoveFirstEntry();
    }

public Map.Entry<K,V> pollLastEntry() {
        return doRemoveLastEntry();
    }

private Map.Entry<K,V> doRemoveFirstEntry() {
        for (Node<K,V> b, n;;) {
            if ((n = (b = head.node).next) == null) //空链表
                return null;
            Node<K,V> f = n.next;
            if (n != b.next) //next节点变了,重新读取b
                continue;
            Object v = n.value;
            if (v == null) {  //n被删除了
                n.helpDelete(b, f);
                continue;
            }
            if (!n.casValue(v, null)) //修改为null失败,重试
                continue;
            if (!n.appendMarker(f) || !b.casNext(n, f)) //插入marker,将n移除,通常第二个可能会失败
                findFirst(); // findFirst的逻辑和上面的for循环的逻辑是一致的,此时也可以continue的
            clearIndexToFirst();
            @SuppressWarnings("unchecked") V vv = (V)v;
            return new AbstractMap.SimpleImmutableEntry<K,V>(n.key, vv);
        }
    }

private Map.Entry<K,V> doRemoveLastEntry() {
        for (;;) {
            //从head往右再往下遍历找到最底层的节点为止,此时该节点还不是最后一个节点
            Node<K,V> b = findPredecessorOfLast();
            Node<K,V> n = b.next; //获取b的下一个节点
            if (n == null) { //
                if (b.isBaseHeader())               //链表是空的
                    return null;
                else
                    continue; //b的next节点空了,b应该是最后一个节点了,重新遍历找到前一个节点
            }
            for (;;) {
                Node<K,V> f = n.next;
                if (n != b.next)                    //next节点变了,重新读取b
                    break;
                Object v = n.value;
                if (v == null) {                    //n被删除了
                    n.helpDelete(b, f);
                    break;
                }
                if (b.value == null || v == n)      //b被删除了
                    break;
                if (f != null) { //n还有后继节点,继续往后遍历
                    b = n;
                    n = f;
                    continue;
                }
                //f为null,此时n就是最后一个节点了,将value置为null
                if (!n.casValue(v, null))
                    break; //修改失败,可能该节点被删除了,则重新开始
                K key = n.key;
                //删除节点n
                if (!n.appendMarker(f) || !b.casNext(n, f))
                    findNode(key);                  //失败后,通过findNode清除节点n和对应的索引节点
                else {                              //删除节点n成功,通过findPredecessor删除对应的索引节点
                    findPredecessor(key, comparator);
                    if (head.right == null)
                        tryReduceLevel(); //尝试降低层级
                }
                @SuppressWarnings("unchecked") V vv = (V)v;
                return new AbstractMap.SimpleImmutableEntry<K,V>(key, vv); //返回键值对
            }
        }
    }

//清除指向第一个节点的索引节点
private void clearIndexToFirst() {
        for (;;) {
            for (Index<K,V> q = head;;) {
                Index<K,V> r = q.right; //q.right就是第一个节点的索引节点了 
                if (r != null && r.indexesDeletedNode()  //r不为空且所引用的节点被删除了
                               && !q.unlink(r)) //将r从q的right链表中移除失败,则重试,可能其他线程在并行删除
                    break;
                if ((q = q.down) == null) { //处理下一个层级的索引节点
                    //如果到最底层了
                    if (head.right == null) //尝试降低层级
                        tryReduceLevel();
                    return;
                }
            }
        }
    }

//从head开始一直往下查找到最底层为止
private Node<K,V> findPredecessorOfLast() {
        for (;;) {
            for (Index<K,V> q = head;;) {
                Index<K,V> d, r;
                if ((r = q.right) != null) {
                    if (r.indexesDeletedNode()) { //r被删除了
                        q.unlink(r);
                        break;    // must restart
                    }
                    //r所引用的节点还有下一个节点,遍历下一个right节点
                    if (r.node.next != null) {
                        q = r; 
                        continue;
                    }
                }
                //right节点为null,遍历下一个层级的节点
                if ((d = q.down) != null)
                    q = d;
                else
                    //遍历到底层了
                    return q.node;
            }
        }
    }

5、keySet / descendingKeySet / Iter

      这两个方法用来遍历当前Map中的key,前者是顺序遍历,后者是倒序遍历,其实现如下:

public NavigableSet<K> keySet() {
            KeySet<K> ks = keySetView;
            return (ks != null) ? ks : (keySetView = new KeySet<K>(this));
        }

public NavigableSet<K> descendingKeySet() {
            return descendingMap().navigableKeySet();
        }

public ConcurrentNavigableMap<K,V> descendingMap() {
        ConcurrentNavigableMap<K,V> dm = descendingMap;
        return (dm != null) ? dm : (descendingMap = new SubMap<K,V>
                                    (this, null, false, null, false, true));
    }

 内部类SubMap的实现下一节会讲解,重点关注内部类KeySet的实现,该类实现了NavigableSet接口,不过其接口实现都是基于保存的ConcurrentNavigableMap引用,如下图:

重点关注其遍历方法的实现,如下:

public Iterator<E> iterator() {
            if (m instanceof ConcurrentSkipListMap)
                return ((ConcurrentSkipListMap<E,Object>)m).keyIterator();
            else
                //KeySet类也适用于内部类SubMap的
                return ((ConcurrentSkipListMap.SubMap<E,Object>)m).keyIterator();
        }

Iterator<K> keyIterator() {
        return new KeyIterator();
    }

 其中KeyIterator继承自内部类Iter,该类的类继承关系如下:

三个子类分别用于遍历key,value和键值对Entry, KeyIterator和Itrer的实现如下:


final class KeyIterator extends Iter<K> {
        public K next() {
            Node<K,V> n = next;
            advance(); //获取下一个节点
            //不同的子类就是此处返回值不一样
            return n.key;
        }
    }

abstract class Iter<T> implements Iterator<T> {
        /** 上一次next方法返回的节点 */
        Node<K,V> lastReturned;
        /** 下一次next方法返回的节点 */
        Node<K,V> next;
        /** 下一次next方法返回的value */
        V nextValue;

        /** Initializes ascending iterator for entire range. */
        Iter() {
            while ((next = findFirst()) != null) { //找到第一个节点
                Object x = next.value;
                if (x != null && x != next) { //校验x是否正常节点
                    @SuppressWarnings("unchecked") V vv = (V)x;
                    nextValue = vv;
                    break;
                }
            }
        }

        public final boolean hasNext() {
            return next != null;
        }

        /** Advances next to higher entry. */
        final void advance() {
            if (next == null)
                throw new NoSuchElementException();
            lastReturned = next;
            while ((next = next.next) != null) { //通过next属性找到下一个节点
                Object x = next.value;
                if (x != null && x != next) { //校验x是否有效节点
                    @SuppressWarnings("unchecked") V vv = (V)x;
                    nextValue = vv;
                    break;
                }
            }
        }

        public void remove() {
            Node<K,V> l = lastReturned;
            if (l == null)
                throw new IllegalStateException();
            //移除目标节点
            ConcurrentSkipListMap.this.remove(l.key);
            lastReturned = null;
        }

    }

6、tailMap / headMap / subMap /SubMap

     上述三个方法都是返回基于当前Map的指定返回的子Map,tailMap返回大于某个key的子Map,headMap 表示小于某个key的子Map,subMap表示返回在两个key之间的子Map。每个方法都有一个重载版本,可以指定是否包含起始值或者终止值,其底层实现都是内部类SubMap,如下:

public ConcurrentNavigableMap<K,V> tailMap(K fromKey) {
        return tailMap(fromKey, true);
    }

public ConcurrentNavigableMap<K,V> tailMap(K fromKey,
                                               boolean inclusive) {
        if (fromKey == null)
            throw new NullPointerException();
        return new SubMap<K,V>
            (this, fromKey, inclusive, null, false, false);
    }

public ConcurrentNavigableMap<K,V> headMap(K toKey) {
        return headMap(toKey, false);
    }

public ConcurrentNavigableMap<K,V> headMap(K toKey,
                                               boolean inclusive) {
        if (toKey == null)
            throw new NullPointerException();
        return new SubMap<K,V>
            (this, null, false, toKey, inclusive, false);
    }

public ConcurrentNavigableMap<K,V> subMap(K fromKey, K toKey) {
        return subMap(fromKey, true, toKey, false);
    }

public ConcurrentNavigableMap<K,V> subMap(K fromKey,
                                              boolean fromInclusive,
                                              K toKey,
                                              boolean toInclusive) {
        if (fromKey == null || toKey == null)
            throw new NullPointerException();
        return new SubMap<K,V>
            (this, fromKey, fromInclusive, toKey, toInclusive, false);
    }

  SubMap也是需要实现ConcurrentNavigableMap接口,大部分方法的实现还是基于父Map的,其包含的属性如下:

        /** 关联的父Map*/
        private final ConcurrentSkipListMap<K,V> m;

        /**起始key */
        private final K lo;

        /**终止key*/
        private final K hi;
        /** 是否包含起始key */
        private final boolean loInclusive;

        /** 是否包含终止key */
        private final boolean hiInclusive;

        /** 是否倒序遍历 */
        private final boolean isDescending;

        //惰性初始化的视图
        private transient KeySet<K> keySetView;
        private transient Set<Map.Entry<K,V>> entrySetView;
        private transient Collection<V> valuesView;

重点关注其遍历方法的实现,如下:

Iterator<K> keyIterator() {
            return new SubMapKeyIterator();
        }

Iterator<V> valueIterator() {
            return new SubMapValueIterator();
        }

Iterator<Map.Entry<K,V>> entryIterator() {
            return new SubMapEntryIterator();
        }

final class SubMapValueIterator extends SubMapIter<V> {
            public V next() {
                V v = nextValue;
                advance(); //获取下一个遍历的节点
                return v;
            }
            public int characteristics() {
                return 0;
            }
        }

final class SubMapKeyIterator extends SubMapIter<K> {
            public K next() {
                Node<K,V> n = next;
                advance(); //获取下一个遍历的节点
                return n.key;
            }
            public int characteristics() {
                return Spliterator.DISTINCT | Spliterator.ORDERED |
                    Spliterator.SORTED;
            }
            public final Comparator<? super K> getComparator() {
                return SubMap.this.comparator();
            }
        }

final class SubMapEntryIterator extends SubMapIter<Map.Entry<K,V>> {
            public Map.Entry<K,V> next() {
                Node<K,V> n = next;
                V v = nextValue;
                advance(); //获取下一个遍历的节点
                return new AbstractMap.SimpleImmutableEntry<K,V>(n.key, v);
            }
            public int characteristics() {
                return Spliterator.DISTINCT;
            }
        }
    

父类SubMapIter的实现如下:



abstract class SubMapIter<T> implements Iterator<T>, Spliterator<T> {   
            Node<K,V> lastReturned //上一次next方法返回的节点
            Node<K,V> next;  //下一次next方法返回的节点        
            V nextValue; //下一次next方法返回的value

            SubMapIter() {
                Comparator<? super K> cmp = m.comparator;
                for (;;) {
                    //获取遍历的起始节点
                    next = isDescending ? hiNode(cmp) : loNode(cmp);
                    if (next == null)
                        break;
                    Object x = next.value;
                    if (x != null && x != next) { //x是有效节点
                        if (! inBounds(next.key, cmp)) //再次检查是否在指定范围内通常在
                            next = null;
                        else {
                            @SuppressWarnings("unchecked") V vv = (V)x;
                            nextValue = vv;
                        }
                        break;
                    }
                }
            }

            public final boolean hasNext() {
                return next != null;
            }

            final void advance() {
                if (next == null)
                    throw new NoSuchElementException();
                lastReturned = next;
                if (isDescending)
                    descend();
                else
                    ascend();
            }
            
            //顺序遍历时获取下一个节点即可
            private void ascend() {
                Comparator<? super K> cmp = m.comparator;
                for (;;) {
                    next = next.next;
                    if (next == null)
                        break;
                    Object x = next.value;
                    if (x != null && x != next) { //判断next节点是否有效
                        if (tooHigh(next.key, cmp)) //如果超过限制则置为null
                            next = null;
                        else {
                            @SuppressWarnings("unchecked") V vv = (V)x;
                            nextValue = vv;
                        }
                        break;
                    }
                }
            }
            
            //倒序遍历
            private void descend() {
                Comparator<? super K> cmp = m.comparator;
                for (;;) {
                    //获取小于上一次节点的最大节点
                    next = m.findNear(lastReturned.key, LT, cmp);
                    if (next == null)
                        break;
                    Object x = next.value;
                    if (x != null && x != next) {
                        if (tooLow(next.key, cmp)) //校验是否低于最低值,如果是则置为null
                            next = null;
                        else {
                            @SuppressWarnings("unchecked") V vv = (V)x;
                            nextValue = vv;
                        }
                        break;
                    }
                }
            }

            public void remove() {
                Node<K,V> l = lastReturned;
                if (l == null)
                    throw new IllegalStateException();
                m.remove(l.key); //移除节点
                lastReturned = null;
            }

            //Spliterator的接口实现
            public Spliterator<T> trySplit() {
                return null;
            }

            public boolean tryAdvance(Consumer<? super T> action) {
                if (hasNext()) {
                    action.accept(next());
                    return true;
                }
                return false;
            }

            public void forEachRemaining(Consumer<? super T> action) {
                while (hasNext())
                    action.accept(next());
            }

            public long estimateSize() {
                return Long.MAX_VALUE;
            }

        }

//获取起始遍历节点
ConcurrentSkipListMap.Node<K,V> loNode(Comparator<? super K> cmp) {
            if (lo == null)  //未指定起始节点
                return m.findFirst();
            else if (loInclusive)
                return m.findNear(lo, GT|EQ, cmp); //获取大于等于lo的最小的节点
            else
                return m.findNear(lo, GT, cmp);
        }

//获取遍历结束节点
ConcurrentSkipListMap.Node<K,V> hiNode(Comparator<? super K> cmp) {
            if (hi == null)
                return m.findLast();
            else if (hiInclusive)
                return m.findNear(hi, LT|EQ, cmp); /获取小于等于ho的最大的节点
            else
                return m.findNear(hi, LT, cmp);
        }

//是否在指定的范围内
boolean inBounds(Object key, Comparator<? super K> cmp) {
            return !tooLow(key, cmp) && !tooHigh(key, cmp);
        }

boolean tooLow(Object key, Comparator<? super K> cmp) {
            int c;
            return (lo != null && ((c = cpr(cmp, key, lo)) < 0 ||
                                   (c == 0 && !loInclusive)));
        }

boolean tooHigh(Object key, Comparator<? super K> cmp) {
            int c;
            return (hi != null && ((c = cpr(cmp, key, hi)) > 0 ||
                                   (c == 0 && !hiInclusive)));
        }

  通过上述代码可知,顺序遍历是很容易的,倒序遍历时因为没有保存对前一个节点的引用,需要从head节点开始重新查找,比较耗时。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值