ConcurrentSkipListMap、ConcurrentSkipListSet源码解读

本文转自:http://brokendreams.iteye.com/blog/2253955 ,难得一见的分析透彻的源码解读文章,值得收藏,好好阅读


功能简介:
  • ConcurrentSkipListMap是一种线程安全的有序的Map。一般我们使用有序Map,不要求线程安全的情况下,可以使用TreeMap,要求线程安全的话,就可以使用ConcurrentSkipListMap。
  • ConcurrentSkipListMap内部的数据结构是SkipList(跳表),内部Entry顺序是由实现了Comparable的key或者构造时指定的Comparator来保证。和TreeMap一样,对ConcurrentSkipListMap中元素的put、get和remove等操作的平均时间复杂度也是O(log(n))。
 
源码分析:
  • 在看内部结构之前,先对跳表这种数据结构有个感性的认识,贴个图:

注:图片来自https://en.wikipedia.org/wiki/Skip_list
 

       ConcurrentSkipListMap源码中也提供了图形化的注释:
Java代码   收藏代码
  1. * Head nodes          Index nodes  
  2. * +-+    right        +-+                      +-+  
  3. * |2|---------------->| |--------------------->| |->null  
  4. * +-+                 +-+                      +-+  
  5. *  | down              |                        |  
  6. *  v                   v                        v  
  7. * +-+            +-+  +-+       +-+            +-+       +-+  
  8. * |1|----------->| |->| |------>| |----------->| |------>| |->null  
  9. * +-+            +-+  +-+       +-+            +-+       +-+  
  10. *  v              |    |         |              |         |  
  11. * Nodes  next     v    v         v              v         v  
  12. * +-+  +-+  +-+  +-+  +-+  +-+  +-+  +-+  +-+  +-+  +-+  +-+  
  13. * | |->|A|->|B|->|C|->|D|->|E|->|F|->|G|->|H|->|I|->|J|->|K|->null  
  14. * +-+  +-+  +-+  +-+  +-+  +-+  +-+  +-+  +-+  +-+  +-+  +-+  
       可见,跳表结构中主要有3中节点:Head节点、Index节点和普通的Node节点。
 
  • 看下源码中这些节点的结构表示:
Java代码   收藏代码
  1. /** 
  2.  * 节点持有key和value,按顺序链接,单向链表。 
  3.  * 中间可能会链接一些处于中间状态的标记节点。 
  4.  * 链表头节点是一个哑(dummy)节点,可以通过head.node访问。 
  5.  * value域之所以定义成Object(而不是E),是因为还要存放一些针对标记节点和头节点的特殊值(non-V) 
  6.  */  
  7. static final class Node<K,V> {  
  8.     final K key;  
  9.     volatile Object value;  
  10.     volatile Node<K,V> next;  
  11.     /** 
  12.      * 创建一个普通节点。 
  13.      */  
  14.     Node(K key, Object value, Node<K,V> next) {  
  15.         this.key = key;  
  16.         this.value = value;  
  17.         this.next = next;  
  18.     }  
  19.     /** 
  20.      * 创建一个标记节点。 
  21.      * 标记节点和普通节点的重要区别是:标记节点的value域指向自身, 
  22.      * 同时标记节点的key为null。key是否为null在一些地方可以用来  
  23.      * 区分标记节点,但无法区分标记节点和base-level链表头节点, 
  24.      * 因为base-level链表头节点的key也是null。 
  25.      */  
  26.     Node(Node<K,V> next) {  
  27.         this.key = null;  
  28.         this.value = this;  
  29.         this.next = next;  
  30.     }  
  31.     /** Updater for casNext */  
  32.     static final AtomicReferenceFieldUpdater<Node, Node>  
  33.         nextUpdater = AtomicReferenceFieldUpdater.newUpdater  
  34.         (Node.class, Node.class"next");  
  35.     /** Updater for casValue */  
  36.     static final AtomicReferenceFieldUpdater<Node, Object>  
  37.         valueUpdater = AtomicReferenceFieldUpdater.newUpdater  
  38.         (Node.class, Object.class"value");  
  39.     /** 
  40.      * compareAndSet value field 
  41.      */  
  42.     boolean casValue(Object cmp, Object val) {  
  43.         return valueUpdater.compareAndSet(this, cmp, val);  
  44.     }  
  45.     /** 
  46.      * compareAndSet next field 
  47.      */  
  48.     boolean casNext(Node<K,V> cmp, Node<K,V> val) {  
  49.         return nextUpdater.compareAndSet(this, cmp, val);  
  50.     }  
  51.     /** 
  52.      * 判断节点是否为标记节点。 
  53.      */  
  54.     boolean isMarker() {  
  55.         return value == this;  
  56.     }  
  57.     /** 
  58.      * 判断节点是否是base-level链表的头节点。 
  59.      */  
  60.     boolean isBaseHeader() {  
  61.         return value == BASE_HEADER;  
  62.     }  
  63.     /** 
  64.      * 尝试在当前节点后面追加一个删除标记节点。 
  65.      */  
  66.     boolean appendMarker(Node<K,V> f) {  
  67.         return casNext(f, new Node<K,V>(f));  
  68.     }  
  69.     /** 
  70.      * 通过追加一个删除标记节点或移除一个标记节点来推进删除。 
  71.      */  
  72.     void helpDelete(Node<K,V> b, Node<K,V> f) {  
  73.         /* 
  74.          * Rechecking links and then doing only one of the 
  75.          * help-out stages per call tends to minimize CAS 
  76.          * interference among helping threads. 
  77.          */  
  78.         if (f == next && this == b.next) {  
  79.             if (f == null || f.value != f) // not already marked  
  80.                 appendMarker(f);  
  81.             else  
  82.                 b.casNext(this, f.next);  
  83.         }  
  84.     }  
  85.     /** 
  86.      * 获取合法的value值。 
  87.      */  
  88.     V getValidValue() {  
  89.         Object v = value;  
  90.         if (v == this || v == BASE_HEADER)  
  91.             //如果是标记节点或者base头节点,返回null。  
  92.             return null;  
  93.         return (V)v;  
  94.     }  
  95.     /** 
  96.      * 为当前映射(Node)创建一个不变(不可修改)的快照。 
  97.      * 如果没有合法值,返回null 
  98.      */  
  99.     AbstractMap.SimpleImmutableEntry<K,V> createSnapshot() {  
  100.         V v = getValidValue();  
  101.         if (v == null)  
  102.             return null;  
  103.         return new AbstractMap.SimpleImmutableEntry<K,V>(key, v);  
  104.     }  
  105. }  

 

       上面方法中重点关注一下删除方法,删除一个节点分为两步:标记和删除。

        1.假设当前节点为n,n的前驱节点为b,n的后继节点为f,如图: 

 

Java代码   收藏代码
  1. +------+       +------+      +------+  
  2. |   b  |------>|   n  |----->|   f  | ...  
  3. +------+       +------+      +------+  
 
      2.现在要删除节点n,那么首先要对n进行标记,如图: 

 

 

Java代码   收藏代码
  1. +------+       +------+      +------+       +------+  
  2. |   b  |------>|   n  |----->|marker|------>|   f  | ...  
  3. +------+       +------+      +------+       +------+  

 

        可见,要删除节点n,首先是往节点n后面追加一个标记节点。

        3.接下来是删除步骤,直接将节点n和后面的标记节点一起删除,如图: 

 

Java代码   收藏代码
  1. +------+                                    +------+  
  2. |   b  |----------------------------------->|   f  | ...  
  3. +------+                                    +------+  
 

 

 

 

       上面是普通节点,再看下Index节点和Head节点:
Java代码   收藏代码
  1. /** 
  2.   * Index节点表示跳表的层级。 
  3.   * 注意到Node和Index都有正向的指针,但是它们的类型和作用都不同, 
  4.   * 无法抽象到一个基类里面。 
  5.   */  
  6.  static class Index<K,V> {  
  7.      final Node<K,V> node;  
  8.      final Index<K,V> down;  
  9.      volatile Index<K,V> right;  
  10.      /** 
  11.       * Creates index node with given values. 
  12.       */  
  13.      Index(Node<K,V> node, Index<K,V> down, Index<K,V> right) {  
  14.          this.node = node;  
  15.          this.down = down;  
  16.          this.right = right;  
  17.      }  
  18.      /** Updater for casRight */  
  19.      static final AtomicReferenceFieldUpdater<Index, Index>  
  20.          rightUpdater = AtomicReferenceFieldUpdater.newUpdater  
  21.          (Index.class, Index.class"right");  
  22.      /** 
  23.       * compareAndSet right field 
  24.       */  
  25.      final boolean casRight(Index<K,V> cmp, Index<K,V> val) {  
  26.          return rightUpdater.compareAndSet(this, cmp, val);  
  27.      }  
  28.      /** 
  29.       * 判断当前Index的Node节点是否被删除。 
  30.       */  
  31.      final boolean indexesDeletedNode() {  
  32.          return node.value == null;  
  33.      }  
  34.      /** 
  35.       * 尝试设置新的后继节点。 
  36.       */  
  37.      final boolean link(Index<K,V> succ, Index<K,V> newSucc) {  
  38.          Node<K,V> n = node;  
  39.          newSucc.right = succ;  
  40.          //需要先检测当前Index的Node是否被删除。  
  41.          return n.value != null && casRight(succ, newSucc);  
  42.      }  
  43.      /** 
  44.       * 尝试设置后继节点(right)为后继的后继(越过后继节点) 
  45.       */  
  46.      final boolean unlink(Index<K,V> succ) {  
  47.          return !indexesDeletedNode() && casRight(succ, succ.right);  
  48.      }  
  49.  }  
 
Java代码   收藏代码
  1. /** 
  2.  * 头节点,每个头节点都包含一个表示层级的域。 
  3.  */  
  4. static final class HeadIndex<K,V> extends Index<K,V> {  
  5.     final int level;  
  6.     HeadIndex(Node<K,V> node, Index<K,V> down, Index<K,V> right, int level) {  
  7.         super(node, down, right);  
  8.         this.level = level;  
  9.     }  
  10. }  
 
       再看下ConcurrentSkipListMap中的结构: 
Java代码   收藏代码
  1. public class ConcurrentSkipListMap<K,V> extends AbstractMap<K,V>  
  2.     implements ConcurrentNavigableMap<K,V>,  
  3.                Cloneable,  
  4.                java.io.Serializable {  
  5.      
  6.     private static final long serialVersionUID = -8627078645895051609L;  
  7.     /** 
  8.      * 用来生成种子的随机数生成器。 
  9.      */  
  10.     private static final Random seedGenerator = new Random();  
  11.     /** 
  12.      * 用来定义base-level的头结点。 
  13.      */  
  14.     private static final Object BASE_HEADER = new Object();  
  15.     /** 
  16.      * 跳表最高层的head index 
  17.      */  
  18.     private transient volatile HeadIndex<K,V> head;  
  19.     /** 
  20.      * 比较器。如果没设置这个比较器,那么久用key的自然序来比较。 
  21.      * @serial 
  22.      */  
  23.     private final Comparator<? super K> comparator;  
  24.     /** 
  25.      * 随机种子,这里没有用volatile修饰,多个线程看到不同的值也没关系。 
  26.      */  
  27.     private transient int randomSeed;  
 
 
  • 大体了解了内部结构,接下来先从简单的构造方法入手分析:

 

Java代码   收藏代码
  1. public ConcurrentSkipListMap() {  
  2.     this.comparator = null;  
  3.     initialize();  
  4. }  
  5.   
  6. public ConcurrentSkipListMap(Comparator<? super K> comparator) {  
  7.     this.comparator = comparator;  
  8.     initialize();  
  9. }  

       两个构造方法除了指定比较器的区别外,都调用了initialize方法,看下这个方法: 

 

 

Java代码   收藏代码
  1. /** 
  2.  * 初始化或重置内部状态。 
  3.  */  
  4. final void initialize() {  
  5.     //将内部一些域置空。  
  6.     keySet = null;  
  7.     entrySet = null;  
  8.     values = null;  
  9.     descendingMap = null;  
  10.     //生成随机种子,这个种子用来生成随机的Level。  
  11.     randomSeed = seedGenerator.nextInt() | 0x0100// 确保非0  
  12.     //生成头节点,该节点value是BASE_HEADER,level是1。  
  13.     head = new HeadIndex<K,V>(new Node<K,V>(null, BASE_HEADER, null),  
  14.                               nullnull1);  
  15. }  

 

 

 

  • 然后分析下put方法:

 

Java代码   收藏代码
  1. public V put(K key, V value) {  
  2.     if (value == null)  
  3.         throw new NullPointerException();  
  4.     return doPut(key, value, false);  
  5. }  
  6. private V doPut(K kkey, V value, boolean onlyIfAbsent) {  
  7.     //将原本的key转化成一个可比较的key。  
  8.     Comparable<? super K> key = comparable(kkey);  
  9.     for (;;) {  
  10.         //通过key找到要插入位置的前驱节点(注意这个节点在base_level上)  
  11.         Node<K,V> b = findPredecessor(key);  
  12.         Node<K,V> n = b.next;  
  13.         for (;;) {  
  14.             if (n != null) {  
  15.                 Node<K,V> f = n.next;  
  16.                 if (n != b.next)               //检测一下,如果读取不一致,说明发生竞争,重试。  
  17.                     break;;  
  18.                 Object v = n.value;  
  19.                 if (v == null) {               //节点n已经被删除了  
  20.                     n.helpDelete(b, f);        //删除动作  
  21.                     break;                     //重试。  
  22.                 }  
  23.                 if (v == n || b.value == null//节点b被删除  
  24.                     break;                     //重试。  
  25.                 int c = key.compareTo(n.key);  
  26.                 if (c > 0) {  
  27.                     //如果c>0,说明当前的节点应该排在n的后面,所以从n后面继续比较。  
  28.                     b = n;  
  29.                     n = f;  
  30.                     continue;  
  31.                 }  
  32.                 if (c == 0) {  
  33.                     //如果onlyIfAbsent为true,那么不进行替换;  
  34.                     //否则需要覆盖旧值。  
  35.                     if (onlyIfAbsent || n.casValue(v, value))  
  36.                         return (V)v;  
  37.                     else  
  38.                         break// 覆盖时竞争失败,重试。  
  39.                 }  
  40.                 // else c < 0; fall through  
  41.             }  
  42.             //1.构造一个新节点。  
  43.             Node<K,V> z = new Node<K,V>(kkey, value, n);  
  44.             //2.尝试插入b和n之间。  
  45.             if (!b.casNext(n, z))  
  46.                 break;         // 如果尝试插入失败,重试一次。  
  47.             //插入成功后,随机生成一个层级。(这个level不会超过31)  
  48.             int level = randomLevel();  
  49.             if (level > 0)  
  50.                 //level大于0,插入index  
  51.                 insertIndex(z, level);  
  52.             return null;  
  53.         }  
  54.     }  
  55. }  

 

 
        大概描述一下put方法(里面的一些细节后面分析):
              1.首先要找出当前节点(其实还没有节点,这里只是给定的key和value)在Node链表(注意这个链表是图中最下层的部分,也就是base-level)中的前驱节点。
              2.然后从前驱节点往后找到要插入的位置,进行插入。
              3.插入成功后,可能会生成一个层级。
 

       这里看一下doPut方法中转化key使用的方法: 

 

Java代码   收藏代码
  1. private Comparable<? super K> comparable(Object key) throws ClassCastException {  
  2.     if (key == null)  
  3.         throw new NullPointerException();  
  4.     if (comparator != null)  
  5.         return new ComparableUsingComparator<K>((K)key, comparator);  
  6.     else  
  7.         return (Comparable<? super K>)key;  
  8. }  
  9. static final class ComparableUsingComparator<K> implements Comparable<K> {  
  10.     final K actualKey;  
  11.     final Comparator<? super K> cmp;  
  12.     ComparableUsingComparator(K key, Comparator<? super K> cmp) {  
  13.         this.actualKey = key;  
  14.         this.cmp = cmp;  
  15.     }  
  16.     public int compareTo(K k2) {  
  17.         return cmp.compare(actualKey, k2);  
  18.     }  
  19. }  

 

       其实就是如果指定了比较器,就使用比较器;没指定比较器,就使用Key的自然序(Key需要实现Comparable接口)。
Java代码   收藏代码
  1. private void addIndex(Index<K,V> idx, HeadIndex<K,V> h, int indexLevel) {  
  2.     // 记录下一个要添加的level,以防重试。  
  3.     int insertionLevel = indexLevel;  
  4.     Comparable<? super K> key = comparable(idx.node.key);  
  5.     if (key == nullthrow new NullPointerException();  
  6.     // 和findPredecessor过程类似,只是在过程中会添加index节点。  
  7.     for (;;) {  
  8.         int j = h.level;  
  9.         Index<K,V> q = h;  
  10.         Index<K,V> r = q.right;  
  11.         Index<K,V> t = idx;  
  12.         for (;;) {  
  13.             if (r != null) {  
  14.                 Node<K,V> n = r.node;  
  15.                 // compare before deletion check avoids needing recheck  
  16.                 int c = key.compareTo(n.key);  
  17.                 if (n.value == null) {  
  18.                     if (!q.unlink(r))  
  19.                         break;  
  20.                     r = q.right;  
  21.                     continue;  
  22.                 }  
  23.                 if (c > 0) {  
  24.                     q = r;  
  25.                     r = r.right;  
  26.                     continue;  
  27.                 }  
  28.             }  
  29.             if (j == insertionLevel) {  
  30.                 // 这里还需要检查t节点是否被删除,如果t节点被删除,就不能插入。  
  31.                 if (t.indexesDeletedNode()) {  
  32.                     findNode(key); // cleans up  
  33.                     return;  
  34.                 }  
  35.                 //尝试将Index t插入q和q的right节点r之间。  
  36.                 if (!q.link(r, t))  
  37.                     break// restart  
  38.                 if (--insertionLevel == 0) {  
  39.                     // 最后还要做一次删除检测。  
  40.                     if (t.indexesDeletedNode())  
  41.                         findNode(key);  
  42.                     return;  
  43.                 }  
  44.             }  
  45.             if (--j >= insertionLevel && j < indexLevel)  
  46.                 t = t.down;  
  47.             q = q.down;  
  48.             r = q.right;  
  49.         }  
  50.     }  
  51. }  
 

       看到上述put的第2步过程,可能会疑惑,这是一个一个往后找的,put的时间复杂度看起来更像O(n)啊,其实玄机就在findPredecessor方法里面,看下这个方法: 

 

Java代码   收藏代码
  1. /** 
  2.  * 返回最底层(base-level)节点链中比给定key小(在“给定节点”左边)的节点, 
  3.  * 如果没找到,那么返回底层链的头节点。 
  4.  * 在查找过程中会顺手删除帮助删除一点标记为删除的节点。 
  5.  */  
  6. private Node<K,V> findPredecessor(Comparable<? super K> key) {  
  7.     if (key == null)  
  8.         throw new NullPointerException(); // don't postpone errors  
  9.     for (;;) {  
  10.         //将最高层的头节点赋给q。  
  11.         Index<K,V> q = head;  
  12.         //将最高层头结点的右节点赋给r。  
  13.         Index<K,V> r = q.right;  
  14.         for (;;) {  
  15.             if (r != null) {  
  16.                 //如果r不为null,找到r中的数据节点n。  
  17.                 Node<K,V> n = r.node;  
  18.                 K k = n.key;  
  19.                 if (n.value == null) {  
  20.                     //如果n已经被删除,那么尝试推进删除。  
  21.                     if (!q.unlink(r))  
  22.                         break;           // 推进删除失败,重试。  
  23.                     r = q.right;         // 再次读取q的头结点,因为上面删除成功后,q的右节点变了。  
  24.                     continue;  
  25.                 }  
  26.                 //n没被删除的话,和key进行比较。  
  27.                 if (key.compareTo(k) > 0) {  
  28.                     //如果给定的key表示的节点在n后面的话,继续往后找。  
  29.                     q = r;  
  30.                     r = r.right;  
  31.                     continue;  
  32.                 }  
  33.             }  
  34.             //如果r为null,那么往下找。  
  35.             //获取q的下节点d  
  36.             Index<K,V> d = q.down;  
  37.             if (d != null) {  
  38.                 //如果d不为null,将d赋给q,d的右节点赋给r,再次循环。  
  39.                 q = d;  
  40.                 r = d.right;  
  41.             } else  
  42.                 return q.node; //如果d为空,说明q就是最底层的节点,返回这个节点。  
  43.         }  
  44.     }  
  45. }  

 

       上述过程其实可以简单的理解为,从最高层的头节点开始找,给定key大于当前节点,就往右找,否则就往下找,一直找到最底层链上的节点,这个节点就是给定key在base_level上的前驱节点。
 

       上面的doPut方法中,最后还有一个生成level index的部分,首先调用randomLevel得到一个level值,如果这个值大于0,就调用insertIndex生成一个index,先看下randomLevel: 

 

Java代码   收藏代码
  1. /** 
  2.  * Returns a random level for inserting a new node. 
  3.  * Hardwired to k=1, p=0.5, max 31 (see above and 
  4.  * Pugh's "Skip List Cookbook", sec 3.4). 
  5.  * 
  6.  * This uses the simplest of the generators described in George 
  7.  * Marsaglia's "Xorshift RNGs" paper.  This is not a high-quality 
  8.  * generator but is acceptable here. 
  9.  */  
  10. private int randomLevel() {  
  11.     int x = randomSeed;  
  12.     x ^= x << 13;  
  13.     x ^= x >>> 17;  
  14.     randomSeed = x ^= x << 5;  
  15.     if ((x & 0x8001) != 0// test highest and lowest bits  
  16.         return 0;  
  17.     int level = 1;  
  18.     while (((x >>>= 1) & 1) != 0) ++level;  
  19.     return level;  
  20. }  

 

       这段代码有些蛋疼...总之就是50%的几率返回0,25%的几率返回1,12.5%的几率返回2...最大返回31。

       继续看insertIndex方法:

 

Java代码   收藏代码
  1. /** 
  2.  * 为给定的数据节点创建和添加Index节点。 
  3.  */  
  4. private void insertIndex(Node<K,V> z, int level) {  
  5.     HeadIndex<K,V> h = head;  
  6.     int max = h.level;  
  7.     if (level <= max) {  
  8.         Index<K,V> idx = null;  
  9.         //如果level比当前的max level小,那么创建level个节点,纵向链接起来  
  10.         //(level2的down节点指向level1、level3的down节点指向level2...)  
  11.         for (int i = 1; i <= level; ++i)  
  12.             idx = new Index<K,V>(z, idx, null);  
  13.         //添加index。  
  14.         addIndex(idx, h, level);  
  15.     } else { //如果level比当前的max level大,添加level。  
  16.         /* 
  17.          * 为了减小其他线程在tryReduceLevel方法中检测空level的干扰, 
  18.          * 新的level添加时右节点就已经初始化好了。它们被依次放到一个 
  19.          * 数组里面,当创建新的head index时,会反向访问它们。 
  20.          */  
  21.         //level设置为max+1  
  22.         level = max + 1;  
  23.         //建立一个长度为level的Index数组,  
  24.         Index<K,V>[] idxs = (Index<K,V>[])new Index[level+1];  
  25.         Index<K,V> idx = null;  
  26.         //还是创建level个节点,纵向链接起来,同时将它们放入Index数组。  
  27.         for (int i = 1; i <= level; ++i)  
  28.             idxs[i] = idx = new Index<K,V>(z, idx, null);  
  29.         HeadIndex<K,V> oldh;  
  30.         int k;  
  31.         for (;;) {  
  32.             oldh = head;  
  33.             int oldLevel = oldh.level;  
  34.             if (level <= oldLevel) { // 竞争失败,跳出。  
  35.                 k = level;  
  36.                 break;  
  37.             }  
  38.             HeadIndex<K,V> newh = oldh;  
  39.             Node<K,V> oldbase = oldh.node;  
  40.             for (int j = oldLevel+1; j <= level; ++j)  
  41.                 /* 
  42.                  *这里创建新的HeadIndex,其数据节点为oldBase,down节点为 
  43.                  *之前的head,right节点为上面Index中level最高的节点,level为j 
  44.                  */  
  45.                 newh = new HeadIndex<K,V>(oldbase, newh, idxs[j], j);  
  46.             //尝试将head设置为新创建的HeadIndex。  
  47.             if (casHead(oldh, newh)) {  
  48.                 k = oldLevel;  
  49.                 break;  
  50.             }  
  51.         }  
  52.         //添加index。  
  53.         addIndex(idxs[k], oldh, k);  
  54.     }  
  55. }  

  

 

 

       再继续看下这个addIndex方法:

Java代码   收藏代码
  1. private void addIndex(Index<K,V> idx, HeadIndex<K,V> h, int indexLevel) {  
  2.     // 记录下一个要添加的level,以防重试。  
  3.     int insertionLevel = indexLevel;  
  4.     Comparable<? super K> key = comparable(idx.node.key);  
  5.     if (key == nullthrow new NullPointerException();  
  6.     // 和findPredecessor过程类似,只是在过程中会添加index节点。  
  7.     for (;;) {  
  8.         int j = h.level;  
  9.         Index<K,V> q = h;  
  10.         Index<K,V> r = q.right;  
  11.         Index<K,V> t = idx;  
  12.         for (;;) {  
  13.             if (r != null) {  
  14.                 Node<K,V> n = r.node;  
  15.                 // compare before deletion check avoids needing recheck  
  16.                 int c = key.compareTo(n.key);  
  17.                 if (n.value == null) {  
  18.                     if (!q.unlink(r))  
  19.                         break;  
  20.                     r = q.right;  
  21.                     continue;  
  22.                 }  
  23.                 if (c > 0) {  
  24.                     q = r;  
  25.                     r = r.right;  
  26.                     continue;  
  27.                 }  
  28.             }  
  29.             if (j == insertionLevel) {  
  30.                 // 这里还需要检查t节点是否被删除,如果t节点被删除,就不能插入。  
  31.                 if (t.indexesDeletedNode()) {  
  32.                     findNode(key); // cleans up  
  33.                     return;  
  34.                 }  
  35.                 //尝试将Index t插入q和q的right节点r之间。  
  36.                 if (!q.link(r, t))  
  37.                     break// restart  
  38.                 if (--insertionLevel == 0) {  
  39.                     // 最后还要做一次删除检测。  
  40.                     if (t.indexesDeletedNode())  
  41.                         findNode(key);  
  42.                     return;  
  43.                 }  
  44.             }  
  45.             if (--j >= insertionLevel && j < indexLevel)  
  46.                 t = t.down;  
  47.             q = q.down;  
  48.             r = q.right;  
  49.         }  
  50.     }  
  51. }  

       这个方法要做的事情其实就是:针对一个给定的节点和level值,将之前建立的从上到下的Index节点链接进来,如图: 

 
              

       当然方法有几个地方要检测当前的idx节点有没有被删除,如果有,要调用一个findNode来做调整,看下这个方法:

Java代码   收藏代码
  1. /** 
  2.  * 通过给定的key查找对应的数据节点,查找过程中会顺便清理一些  
  3.  * 已经标记为删除的节点。 
  4.  * 一些地方会调用这个方法,不是为了查找节点,而是为了使用清理  
  5.  * 删除节点的这个"副作用"。 
  6.  * 
  7.  * 下列情况出现时,会重新遍历: 
  8.  * 
  9.  *   (1) 在读取了n的next域之后,n不再是b当前的后继节点了,这意味 
  10.  *       着我们没有和之前保持一致的3节点(b->n->f)快照,所以无法 
  11.  *       删除后续节点。 
  12.  * 
  13.  *   (2) 节点n的value域为null,说明n已经被删除。这种情况下,我们 
  14.  *       先帮助推进n节点的删除,然后再重试。 
  15.  * 
  16.  *   (3) n是一个标记节点或者n的前驱节点的value域为null。意味着  
  17.  *       findPredecessor方法会返回一个被删除的节点。我们无法移  
  18.  *       除这节点,因为无法确定它的前驱节点。所以再次调用findPredecessor  
  19.  *       (findPredecessor方法中会处理这个情况)并返正确的前驱节点。 
  20.  */  
  21. private Node<K,V> findNode(Comparable<? super K> key) {  
  22.     for (;;) {  
  23.         //找到key对应的在base_level上的前驱节点。  
  24.         Node<K,V> b = findPredecessor(key);  
  25.         Node<K,V> n = b.next;  
  26.         for (;;) {  
  27.             if (n == null)  
  28.                 return null;  
  29.             Node<K,V> f = n.next;  
  30.             if (n != b.next)                // 读取不一致,说明发生竞争,重试。  
  31.                 break;  
  32.             Object v = n.value;  
  33.             if (v == null) {                // n被删除了,帮助推进n的删除,重试。  
  34.                 n.helpDelete(b, f);  
  35.                 break;  
  36.             }  
  37.             if (v == n || b.value == null)  // b被删除了,重试。  
  38.                 break;  
  39.             //开始比较  
  40.             int c = key.compareTo(n.key);  
  41.             if (c == 0)  
  42.                 return n; //找到对应节点。  
  43.             if (c < 0)  
  44.                 return null;  
  45.             b = n;  
  46.             n = f;  
  47.         }  
  48.     }  
  49. }  
       再次更细致的总结一下put方法:
              1.首先要根据给定的key找出在base_level链表中对应的前驱节点(从结构图的左上角往右或往下一路找过来),注意put方法使用的log(n)时间主要体现在这个过程,这个查找过程中会顺便帮忙推进一些节点的删除。
              2.找到前驱节点后,然后从这个前驱节点往后找到要插入的位置(注意当前已经在base_level上,所以只需要往后找),这个查找过程中也会顺便帮忙推进一些节点的删除。。
              3.找到了要插入的位置,尝试插入,如果竞争导致插入失败,返回到第1步重试;如果插入成功,接下来会随机生成一个level,如果这个level大于0,需要将插入的节点在垂直线上生成level(level<=maxLevel + 1)个Index节点。
 
  • 分析完了put方法,再看下get方法:
Java代码   收藏代码
  1. public V get(Object key) {  
  2.     return doGet(key);  
  3. }  
  4. private V doGet(Object okey) {  
  5.     //转换成可比较的Key。  
  6.     Comparable<? super K> key = comparable(okey);  
  7.     Node<K,V> bound = null;  
  8.     Index<K,V> q = head;  
  9.     Index<K,V> r = q.right;  
  10.     Node<K,V> n;  
  11.     K k;  
  12.     int c;  
  13.     for (;;) {  
  14.         Index<K,V> d;  
  15.         // 向右遍历,一直到null或者当前给定key(对应的节点)大的节点(bound)  
  16.         // 当前给定key对应的节点应该在bound的左边。  
  17.         if (r != null && (n = r.node) != bound && (k = n.key) != null) {  
  18.             if ((c = key.compareTo(k)) > 0) {  
  19.                 q = r;  
  20.                 r = r.right;  
  21.                 continue;  
  22.             } else if (c == 0) {  
  23.                 Object v = n.value;  
  24.                 return (v != null)? (V)v : getUsingFindNode(key);  
  25.             } else  
  26.                 bound = n;  
  27.         }  
  28.         // 往下找。  
  29.         if ((d = q.down) != null) {  
  30.             q = d;  
  31.             r = d.right;  
  32.         } else  
  33.             break;  
  34.     }  
  35.     // 现在到了base_level,往后找就可以了。  
  36.     for (n = q.node.next;  n != null; n = n.next) {  
  37.         if ((k = n.key) != null) {  
  38.             if ((c = key.compareTo(k)) == 0) {  
  39.                 Object v = n.value;  
  40.                 return (v != null)? (V)v : getUsingFindNode(key);  
  41.             } else if (c < 0)  
  42.                 break;  
  43.         }  
  44.     }  
  45.     return null;  
  46. }  
       可见get的过程大概是这样:从最左最高的头节点开始往右或者往下(通过key的比较)遍历,一直找到base_level,然后往后一直找到给定节点,找不到的话返回null。

       代码中还会看到,如果找到了节点,还会判断节点上的value是否为null。如果不为null,直接返回这个value;如果为null,说明这个节点被删除了(正在删除过程中),那么需要调用一个getUsingFindNode方法,看下这个方法: 

Java代码   收藏代码
  1. private V getUsingFindNode(Comparable<? super K> key) {  
  2.     for (;;) {  
  3.         Node<K,V> n = findNode(key);  
  4.         if (n == null)  
  5.             return null;  
  6.         Object v = n.value;  
  7.         if (v != null)  
  8.             return (V)v;  
  9.     }  
  10. }  
       此方法中会再次调用前面分析过的findNode方法来查找一个key对应的Node,注意方法中外侧还是包了一层无限循环,为的是避免由于竞争导致findNode方法返回的Node又是一个被删除的节点。
 
  • 继续看下containsKey方法:
Java代码   收藏代码
  1. public boolean containsKey(Object key) {  
  2.     return doGet(key) != null;  
  3. }  
       containsKey实现非常简单,直接基于doGet方法来做。
 
  • 接着看下remove方法:
Java代码   收藏代码
  1. public V remove(Object key) {  
  2.     return doRemove(key, null);  
  3. }  
  4. final V doRemove(Object okey, Object value) {  
  5.     //转换成可比较的key  
  6.     Comparable<? super K> key = comparable(okey);  
  7.     for (;;) {  
  8.         //找到key在base_level链上的前驱节点。  
  9.         Node<K,V> b = findPredecessor(key);  
  10.         Node<K,V> n = b.next;  
  11.         for (;;) {  
  12.             if (n == null)  
  13.                 return null;  
  14.             Node<K,V> f = n.next;  
  15.             if (n != b.next)                    // 读取不一致,重试。  
  16.                 break;  
  17.             Object v = n.value;  
  18.             if (v == null) {                    // 如果n被删除了,帮助推进删除,然后重试。  
  19.                 n.helpDelete(b, f);  
  20.                 break;  
  21.             }  
  22.             if (v == n || b.value == null)      // 如果b被删除了,重试。  
  23.                 break;  
  24.             int c = key.compareTo(n.key);  
  25.             if (c < 0)  
  26.                 return null//如果比找到的前驱节点的后继节点小,说明没有指定的key对应的节点,返回null。  
  27.             if (c > 0) {  
  28.                 //如果比找到的前驱节点的后继节点大,说明目标节点在这个节点后面,往后找。  
  29.                 b = n;  
  30.                 n = f;  
  31.                 continue;  
  32.             }  
  33.             if (value != null && !value.equals(v))  
  34.                 return null;//给定的value和链表中的value不一致,删除失败。  
  35.             if (!n.casValue(v, null))//首先尝试将要删除的目标节点n的value置空。  
  36.                 break//如果失败,说明发生竞争,重试。  
  37.             /* 
  38.              * 如果上一步将n的value置空成功,接下来首先尝试将n的后面追加一个标记节点, 
  39.              * 成功的话,再尝试将n和标记节点一起移除,这两部有任何一步失败,都会调用 
  40.              * findNode来完成删除(利用findNode方法的副作用) 
  41.              */  
  42.             if (!n.appendMarker(f) || !b.casNext(n, f))  
  43.                 findNode(key);                  // Retry via findNode  
  44.             else {  
  45.                 /* 
  46.                  * 如果上面的n.appendMarker(f)和b.casNext(n, f)都调用成功, 
  47.                  * 然后就会调用这个方法,注意这里其实也是使用这个方法的 
  48.                  * 副作用来删除节点n的Index节点。 
  49.                  */  
  50.                 findPredecessor(key);             
  51.                 if (head.right == null)  
  52.                     // 删除了一些Index之后,这里判断一下head的right节点是否为null,  
  53.                     // 如果为null,说明最高层的Index链已经不存在数据,可以删掉了。  
  54.                     tryReduceLevel();  
  55.             }  
  56.             return (V)v;  
  57.         }  
  58.     }  
  59. }  

 

       看一下上面方法中最后调用的tryReduceLevel方法: 

Java代码   收藏代码
  1. private void tryReduceLevel() {  
  2.     HeadIndex<K,V> h = head;  
  3.     HeadIndex<K,V> d;  
  4.     HeadIndex<K,V> e;  
  5.     if (h.level > 3 &&  
  6.         (d = (HeadIndex<K,V>)h.down) != null &&  
  7.         (e = (HeadIndex<K,V>)d.down) != null &&  
  8.         e.right == null &&  
  9.         d.right == null &&  
  10.         h.right == null &&  
  11.         casHead(h, d) && // try to set  
  12.         h.right != null// recheck  
  13.         casHead(d, h);   // try to backout  
  14. }  
       这个方法看起来有点绕,其实是说:如果最高的前三个HeadIndex都为null(当前看起来),那么就将level减少1层,其实就是将h(当前的head)设置为d(head的下一层),设置完成之后,还有检测h(之前的head)的right是否为null(因为可能刚才由于竞争的原因,导致看到h的right为null),如果这会儿又不是null,那么还得回退回来,再次将head设置为h。
 
  • 再看下containsValue方法:
Java代码   收藏代码
  1. public boolean containsValue(Object value) {  
  2.     if (value == null)  
  3.         throw new NullPointerException();  
  4.     for (Node<K,V> n = findFirst(); n != null; n = n.next) {  
  5.         V v = n.getValidValue();  
  6.         if (v != null && value.equals(v))  
  7.             return true;  
  8.     }  
  9.     return false;  
  10. }  
       containsValue的实现也比较简答,就是先找到base_level链的第一个节点,然后一直往后找,比较value值。

       看下上面用到的findFirst方法: 

Java代码   收藏代码
  1. Node<K,V> findFirst() {  
  2.     for (;;) {  
  3.         Node<K,V> b = head.node;  
  4.         Node<K,V> n = b.next;  
  5.         if (n == null)  
  6.             return null;  
  7.         if (n.value != null)  
  8.             return n;  
  9.         n.helpDelete(b, n.next);  
  10.     }  
  11. }  

       就是找到head中node(BASE_HEADER节点)的next,有可能next节点被删除了,所以会做检测,删除的话,推进一下删除,然后继续获取。size和isEmpty也是基于这个方法实现的: 

Java代码   收藏代码
  1. public int size() {  
  2.     long count = 0;  
  3.     for (Node<K,V> n = findFirst(); n != null; n = n.next) {  
  4.         if (n.getValidValue() != null)  
  5.             ++count;  
  6.     }  
  7.     return (count >= Integer.MAX_VALUE)? Integer.MAX_VALUE : (int)count;  
  8. }  
  9. public boolean isEmpty() {  
  10.     return findFirst() == null;  
  11. }  

 

  • ConcurrentSkipListMap实现了ConcurrentMap接口,看下这些接口方法的实现:
Java代码   收藏代码
  1. public V putIfAbsent(K key, V value) {  
  2.      if (value == null)  
  3.          throw new NullPointerException();  
  4.      return doPut(key, value, true);  
  5.  }  
  6.   
  7.  public boolean remove(Object key, Object value) {  
  8.      if (key == null)  
  9.          throw new NullPointerException();  
  10.      if (value == null)  
  11.          return false;  
  12.      return doRemove(key, value) != null;  
  13.  }  
  14.   
  15.  public boolean replace(K key, V oldValue, V newValue) {  
  16.      if (oldValue == null || newValue == null)  
  17.          throw new NullPointerException();  
  18.      Comparable<? super K> k = comparable(key);  
  19.      for (;;) {  
  20.          Node<K,V> n = findNode(k);  
  21.          if (n == null)  
  22.              return false;  
  23.          Object v = n.value;  
  24.          if (v != null) {  
  25.              if (!oldValue.equals(v))  
  26.                  return false;  
  27.              if (n.casValue(v, newValue))  
  28.                  return true;  
  29.          }  
  30.      }  
  31.  }  
  32.   
  33.  public V replace(K key, V value) {  
  34.      if (value == null)  
  35.          throw new NullPointerException();  
  36.      Comparable<? super K> k = comparable(key);  
  37.      for (;;) {  
  38.          Node<K,V> n = findNode(k);  
  39.          if (n == null)  
  40.              return null;  
  41.          Object v = n.value;  
  42.          if (v != null && n.casValue(v, value))  
  43.              return (V)v;  
  44.      }  
  45.  }  
       ConcurrentMap API方法实现中使用到的方法前面都分析过了,可以参考下前面的分析。
 
  • ConcurrentSkipListMap同样实现了SortedMap接口,看下相关接口方法的实现:
Java代码   收藏代码
  1. public K lastKey() {  
  2.     Node<K,V> n = findLast();  
  3.     if (n == null)  
  4.         throw new NoSuchElementException();  
  5.     return n.key;  
  6. }  

       这里出现了一个findLast方法,之前没分析过,看下: 

Java代码   收藏代码
  1. Node<K,V> findLast() {  
  2.     Index<K,V> q = head;  
  3.     for (;;) {  
  4.         Index<K,V> d, r;  
  5.         if ((r = q.right) != null) {  
  6.             if (r.indexesDeletedNode()) { //如果发现节点已经删除的Index,顺便移除。  
  7.                 q.unlink(r);  
  8.                 q = head; // 重试。  
  9.             }  
  10.             else  
  11.                 q = r; //向右找  
  12.         } else if ((d = q.down) != null) {  
  13.             q = d; //向下找  
  14.         } else {  
  15.             //现在到了base_level链上,向后找。  
  16.             Node<K,V> b = q.node;  
  17.             Node<K,V> n = b.next;  
  18.             for (;;) {  
  19.                 if (n == null)  
  20.                     //最后定位到节点后需要检测一下是不是baseHead  
  21.                     return (b.isBaseHeader())? null : b;   
  22.                 Node<K,V> f = n.next;            // 读取不一致,有竞争发生,重试。  
  23.                 if (n != b.next)  
  24.                     break;  
  25.                 Object v = n.value;  
  26.                 if (v == null) {                 // n节点被删除,重试。  
  27.                     n.helpDelete(b, f);  
  28.                     break;  
  29.                 }  
  30.                 if (v == n || b.value == null)   // b节点被删除,重试。  
  31.                     break;  
  32.                 b = n;  
  33.                 n = f;  
  34.             }  
  35.             q = head; // restart  
  36.         }  
  37.     }  
  38. }  

 

       再看一个方法:
Java代码   收藏代码
  1. public Map.Entry<K,V> lowerEntry(K key) {  
  2.     return getNear(key, LT);  
  3. }  
       这个方法的意思是找到一个比给定key小的所有key里面最大的key对应的Entry,里面调用了getNear方法: 
Java代码   收藏代码
  1. AbstractMap.SimpleImmutableEntry<K,V> getNear(K key, int rel) {  
  2.     for (;;) {  
  3.         Node<K,V> n = findNear(key, rel);  
  4.         if (n == null)  
  5.             return null;  
  6.         AbstractMap.SimpleImmutableEntry<K,V> e = n.createSnapshot();  
  7.         if (e != null)  
  8.             return e;  
  9.     }  
  10. }  
       getNear方法里面首先通过findNear方法找到指定的Node,然后通过createSnapshot方法返回一个Entry,这个方法最开始的时候看到过。下面重点看下这个findNear方法: 
Java代码   收藏代码
  1. private static final int EQ = 1;  
  2. private static final int LT = 2;  
  3. private static final int GT = 0// Actually checked as !LT  
  4. /** 
  5.  * Utility for ceiling, floor, lower, higher methods. 
  6.  * @param kkey the key 
  7.  * @param rel the relation -- OR'ed combination of EQ, LT, GT 
  8.  * @return nearest node fitting relation, or null if no such 
  9.  */  
  10. Node<K,V> findNear(K kkey, int rel) {  
  11.     Comparable<? super K> key = comparable(kkey);  
  12.     for (;;) {  
  13.         Node<K,V> b = findPredecessor(key);  
  14.         Node<K,V> n = b.next;  
  15.         for (;;) {  
  16.             if (n == null)  
  17.                 return ((rel & LT) == 0 || b.isBaseHeader())? null : b; //出口1  
  18.             Node<K,V> f = n.next;  
  19.             if (n != b.next)                  // inconsistent read  
  20.                 break;  
  21.             Object v = n.value;  
  22.             if (v == null) {                  // n is deleted  
  23.                 n.helpDelete(b, f);  
  24.                 break;  
  25.             }  
  26.             if (v == n || b.value == null)    // b is deleted  
  27.                 break;  
  28.             int c = key.compareTo(n.key);  
  29.             if ((c == 0 && (rel & EQ) != 0) ||  
  30.                 (c <  0 && (rel & LT) == 0))  
  31.                 return n; //出口2  
  32.             if ( c <= 0 && (rel & LT) != 0)  
  33.                 return (b.isBaseHeader())? null : b; //出口3  
  34.             b = n;  
  35.             n = f;  
  36.         }  
  37.     }  
  38. }  
       这个方法整体流程其实和findNode类似,都是从head开始先找到base_level的上给定key的前驱节点,然后再往后找。区别是这里传入关系参数-EQ、LT、GT(GT在代码里面没直接用,而是通过没有LT来判断),我们可以通过lowerEntry方法来分析,lowerEntry方法中间接调用的findNear方法传入的是LT,所以当在findNear方法中定位到目标节点n的时候,节点关系是这样的:[b->n->f],节点key和给定key的大小关系是这样的:[b<k<=n<f],所以代码会从findNear中的出口3(见注释)返回。
       接着分析下floorEntry: 
Java代码   收藏代码
  1. public Map.Entry<K,V> floorEntry(K key) {  
  2.     return getNear(key, LT|EQ);  
  3. }  
       floorEntry方法中间接调用的findNear方法传入的是LT|EQ,所以当在findNear方法中定位到目标节点n的时候,节点关系是这样的:[b->n->f],节点key和给定key的大小关系是这样的:[b<k<=n<f],这里分两种情况:1.如果k<n,那么会从出口3退出,返回b;2.如果k=n,那么会从出口2退出(满足条件(c == 0 && (rel & EQ) != 0)),返回n。
       继续分析下ceilingEntry:
Java代码   收藏代码
  1. public Map.Entry<K,V> ceilingEntry(K key) {  
  2.     return getNear(key, GT|EQ);  
  3. }  
       ceilingEntry方法中间接调用的findNear方法传入的是GT|EQ,所以当在findNear方法中定位到目标节点n的时候,节点关系是这样的:[b->n->f],节点key和给定key的大小关系是这样的:[b<k<=n<f],这里分两种情况:1.如果k<n(k>b),那么会从出口2退出(满足条件(c <  0 && (rel & LT) == 0)),返回b;2.如果k=n,那么也会从出口2退出(满足条件(c == 0 && (rel & EQ) != 0)),返回n。
       最后分析下higherEntry: 
Java代码   收藏代码
  1. public Map.Entry<K,V> higherEntry(K key) {  
  2.     return getNear(key, GT);  
  3. }  
       higherEntry方法中间接调用的findNear方法传入的是GT,所以当在findNear方法中定位到目标节点n的时候,节点关系是这样的:[b->n->f],节点key和给定key的大小关系是这样的:[b<k<=n<f],这里分两种情况:1.如果k<n,那么会从出口2退出(满足条件(c <  0 && (rel & LT) == 0)),返回b;2.如果k=n,那么会进入下一次循环,关系变成这样:[b<n=k<f],这时会从出口2退出(满足条件(c <  0 && (rel & LT) == 0)),这时返回的是f。
 
       当然,上面分析的所有方法,都会遇到在findNear中遇到n==null的可能,这时关系图如下[b<k-null],k一定大于b,所以只有传入LT,才可以返回b;否则都是null。而且如果b本身是BaseHead,也只能返回null。
 
  • ConcurrentSkipListMap分析到这里,一些关键的地方已经分析到了,至于其他没覆盖到的方法,基本都是基于上面分析的方法或思路来实现的,这里就不一一分析了。
       最后提一下,看这个类源码的时候,一定要注意几个方面:
       1.注意数据结构,可以仔细理解上面给出的结构图,或者先了解下跳表的背景知识。
       2.注意删除节点是一个标记删除的过程,分两步,记住这个过程;而且源码中好多方法在遍历的过程中都会帮助推进删除。
       3.由于此类是一种无锁并发的实现,所以代码细节上会考虑各种竞争情况,导致代码比较复杂,所以阅读的时候也要考虑全面,仔细看注释,揣摩作者的意图。
 
  • 最后,ConcurrentSkipListSet基于ConcurrentSkipListMap实现的:
Java代码   收藏代码
  1. public class ConcurrentSkipListSet<E>  
  2.     extends AbstractSet<E>  
  3.     implements NavigableSet<E>, Cloneable, java.io.Serializable {  
  4.     private static final long serialVersionUID = -2479143111061671589L;  
  5.   
  6.     private final ConcurrentNavigableMap<E,Object> m;  
  7.   
  8.     public ConcurrentSkipListSet() {  
  9.         m = new ConcurrentSkipListMap<E,Object>();  
  10.     }  
  11.     ...  
  12.     public boolean add(E e) {  
  13.     return m.putIfAbsent(e, Boolean.TRUE) == null;  
  14.     }  
  15.     ...  
       方法实现,都由内部的ConcurrentSkipListMap代理实现,value默认填充Boolean.TRUE。
 
 

       ok,代码解析完毕! 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值