java进阶笔记线程与并发之CountedCompleter

说明

CountedCompleter是ForkJoinTask的一个子类。

其可以简单理解为处理业务和数量有关的一些FJT,一般分为如下几类:

  • 和数量无关,一般不使用CountedCompleter
  • 一个: findAny、searchFirst这种操作,只要在集合、流中找到一个就表示整个任务完成的
  • 指定数量的: 比如有的业务需要触发多次完成的。
  • 可能需要有序完成的,有序完成可以通过CountedCompleter的完成器来委婉的实现此功能。

定义:

  • FJT = ForkJoinTask

  • FJP = ForkJoinPool

    说明: 需要看如何使用的,直接查看下面的官方示例。

源码

/**
* 这是一个FJT,会在任务完成时检查是否有等待的任务,若没有则触发一个完成动作。
* 总的来说,与其他形式的forkjointask相比,CountedCompleters在存在子任务停滞
* 和阻塞的情况下更健壮,但对编程的直觉较差。CountedCompleter的使用类似于其他基
* 于完成的组件(例如 java.nio.channels.CompletionHandler),除了需要多个挂
* 起的完成来触发完成操作 onCompletion(CountedCompleter),而不是一个。
* 除非另外初始化,否则 getPendingCount pending count从0开始,但是可以使用 link
* setPendingCount、addToPendingCount和compareAndSetPendingCount(原子地)更
* 改。在调用 tryComplete时,如果挂起的动作计数为非零,则递减;否则,将执行完成操作,
* 如果这个完成器本身有一个完成器,则继续使用它的完成器。与相关的同步组件
* (如 java.util.concurrent.Phaser 和 java.util.concurrent.Semaphore )相同。
* 这些方法只影响内部计数;他们没有建立任何进一步的内部簿记。特别是,未完成任务的标识没有
* 得到维护。如下所示,您可以创建子类来记录一些或所有挂起的任务或它们的结果。如下所示,
* 还提供了支持自定义完成遍历的实用程序方法。但是,因为CountedCompleters只提供了基
* 本的同步机制,所以创建进一步的抽象子类来维护链接、字段和其他适合于一组相关用法的支
* 持方法可能是有用的。
*
* 具体的CountedCompleter类必须定义方法{@link #compute},在大多数情况下(如下所
* 示),在返回之前应该调用{@code tryComplete()}一次。该类还可以选择性地覆盖方法
* {@link #onCompletion(CountedCompleter)}来在正常完成时执行一个动作,以及方
* 法{@link #onExceptionalCompletion(Throwable, CountedCompleter)}来在任
* 何异常时执行一个动作。
*
* CountedCompleters通常不产生结果,在这种情况下,它们通常被声明为
* {@code CountedCompleter},并且总是返回{@code null}作为结果值。在其他情况下,
* 您应该覆盖方法{@link #getRawResult}来提供来自{@code join()、invoke()}和相
* 关方法的结果。通常,该方法应该返回CountedCompleter对象的一个字段(或一个或多个字
* 段的函数)的值,该对象在完成时保存结果。默认情况下,方法{@link #setRawResult}在
* CountedCompleters中不起作用。重写此方法以维护包含结果数据的其他对象或字段是可能
* 的,但很少适用。
*
* 一个CountedCompleter本身没有一个completer(即,其中{@link #getCompleter}返
* 回{@code null})可以作为一个常规的ForkJoinTask与这个增加的功能。
* 但是,任何有另一个completer的completer只是作为其他计算的内部助手,所以它自己的任
 * 务状态(如{@link    * ForkJoinTask#isDone}等方法中报告的)是任意的;这种状态只有
* 在显式调用{@link #complete}、{@link ForkJoinTask#cancel}、
* {@link ForkJoinTask# complete(Throwable)}或方法{@code compute}异常完成时
* 才会改变。在任何异常完成之后,如果存在一个任务的完成器,并且它还没有完成,则可以将异
* 常传递给任务的完成器(以及它的完成器,等等)。类似地,取消内部的CountedCompleter只
* 会对该完井器产生局部影响,所以通常不会有用。
*
* <p> 用例 </b>
* bookkeeping=簿记;
* 并行递归分解。CountedCompleters可能被安排在与{@link RecursiveAction}类似的树
* 中,尽管设置它们所涉及的结构通常是不同的。这里,每个任务的完成者是计算树中的父任务。
* 即使它们需要更多的簿记,当将可能耗时的操作(不能进一步细分)应用到数组或集合的每个元素
* 时,CountedCompleters可能是更好的选择;特别是当某些元素完成操作所需的时间与其他元
* 素明显不同时,这可能是由于内部变化(例如I/O),也可能是由于诸如垃圾收集之类的辅助效果。
* 因为CountedCompleters提供了它们自己的延续,其他线程不需要阻塞来执行它们。
*
* 例如,下面是一个类的初始版本,它使用两个递归分解来将工作划分为单个部分(叶子任务)。即使
* 将工作划分为单独的调用,基于树的技术通常也比直接fork叶子任务更好,因为它们减少了线程间
* 的通信并改善了负载平衡。在递归情况下,每对要完成的叶子任务的第二个触发其父任务的完成(
* 因为没有执行结果组合,所以方法{@code onCompletion}的默认无操作实现没有被覆盖)。
* 静态实用程序方法设置基本任务并调用它(这里使用{@link ForkJoinPool#commonPool()
* }隐式地调用它)。
*
*
* 触发器。有些CountedCompleters本身并不是fork的,而是作为其他设计中的管道的一部分;
* 包括那些完成一个或多个异步任务触发另一个异步任务的部分。例如:
* For example:
*
* class HeaderBuilder extends CountedCompleter<...> { ... }
* class BodyBuilder extends CountedCompleter<...> { ... }
* class PacketSender extends CountedCompleter<...> {
 *   PacketSender(...) { super(null, 1); ... } // 在第二次完成时触发onCompletion
 *   public void compute() { } // never called
 *   public void onCompletion(CountedCompleter<?> caller) { sendPacket(); }
* }
* // sample use:
* PacketSender p = new PacketSender();
* new HeaderBuilder(p, ...).fork();
* new BodyBuilder(p, ...).fork();
* }
*
* @since 1.8
* @author Doug Lea
*/
public abstract class CountedCompleter<T> extends ForkJoinTask<T> {
    private static final long serialVersionUID = 5232453752276485070L;
 
    /** This task's completer, or null if none */
    //PS:一个链表结构,完成的触发顺序则是先进后出的模式
    final CountedCompleter<?> completer;
    /** 在完成前等待的任务数量 */
    volatile int pending;
 
    /**
     * PS: 传入的completer一般是‘父’任务的实例引用
     * @param completer this task's completer, or {@code null} if none
     * @param 初始PendingCount
     */
    protected CountedCompleter(CountedCompleter<?> completer,
                               int initialPendingCount) {
        this.completer = completer;
        this.pending = initialPendingCount;
    }
 
    protected CountedCompleter(CountedCompleter<?> completer) {
        this.completer = completer;
    }
    protected CountedCompleter() {
        this.completer = null;
    }
 
    /**
     * 这个任务完成的主要计算。
     */
    public abstract void compute();
 
    /**
     * 调用了无条件complete或者在调用tryComplete时,如果等待的任务数量是0,则触发此方法。
     * 默认情况下,此方法不执行任何操作。您可以通过检查给定调用方参数的标识来区分情况。
     * 如果不等于this,那么它通常是一个子任务,可能包含要组合的结果(和/或到其他结果的链接)。
     *
     * @param caller the task invoking this method (which may
     * be this task itself)
     */
    public void onCompletion(CountedCompleter<?> caller) {
    }
 
    /**
     * 调用方法completeexception (Throwable)或方法compute抛出异常时执行操作,
     * 且此任务尚未正常完成。进入这个方法时,这个任务FJTk#iscompletedwrong。
     * 这个方法的返回值控制了进一步的传播:如果 true 并且这个任务有一个未完成的完成
     * 器,那么这个完成器也会异常地完成,与这个完成器有相同的异常。这个方法的默认实
     * 现只返回 true。
     * @param ex the exception
     * @param caller the task invoking this method (which may
     * be this task itself)
     * @return {@code true} if this exception should be propagated to this
     * task's completer, if one exists
     */
    public boolean onExceptionalCompletion(Throwable ex, CountedCompleter<?> caller) {
        return true;
    }
 
    /**
     * 返回在此任务的构造函数中建立的完成器, or null if none.
     *
     * @return the completer
     */
    public final CountedCompleter<?> getCompleter() {
        return completer;
    }
 
    /**
     * return当前等待的数量
     *
     * @return the current pending count
     */
    public final int getPendingCount() {
        return pending;
    }
 
    /**
     * @param count the count
     */
    public final void setPendingCount(int count) {
        pending = count;
    }
 
    /**
     * (原子地)将给定的值添加到挂起的计数
     * @param delta the value to add
     */
    public final void addToPendingCount(int delta) {
        U.getAndAddInt(this, PENDING, delta);
    }
 
    /**
     * 只有在当前持有给定的期望值时,才会(原子地)将挂起的计数设置为给定的计数。
     *
     * @param expected the expected value
     * @param count the new value
     * @return {@code true} if successful
     */
    public final boolean compareAndSetPendingCount(int expected, int count) {
        return U.compareAndSwapInt(this, PENDING, expected, count);
    }
 
    /**
     * If the pending count is nonzero, (atomically) decrements it.
     *
     * @return the initial (undecremented) pending count holding on entry
     * to this method
     */
    public final int decrementPendingCountUnlessZero() {
        int c;
        do {} while ((c = pending) != 0 &&
                     !U.compareAndSwapInt(this, PENDING, c, c - 1));
        return c;
    }
 
    /**
     * Returns the root of the current computation; i.e., this
     * task if it has no completer, else its completer's root.
     * PS: 遍历单向链表,找到root节点
     * @return the root of the current computation
     */
    public final CountedCompleter<?> getRoot() {
        CountedCompleter<?> a = this, p;
        while ((p = a.completer) != null)
            a = p;
        return a;
    }
 
    /**
     * 如果挂起的计数为非零,则减少计数;否则调用 onCompletion(CountedCompleter),
     * 然后类似地尝试完成此任务的completer(如果存在的话),否则将此任务标记为完成。
     * PS:a.onCompletion(s); 表示完成的当前的任务,s = a 这里指向父任务
     *     如果父任务==null,则完成整个任务链的任务;否则重新进入循环执行逻辑
     */
    public final void tryComplete() {
        CountedCompleter<?> a = this, s = a;
        for (int c;;) {
            if ((c = a.pending) == 0) {
                a.onCompletion(s);
                if ((a = (s = a).completer) == null) {
                    s.quietlyComplete();
                    return;
                }
            }
            else if (U.compareAndSwapInt(a, PENDING, c, c - 1))
                return;
        }
    }
 
    /**
     * PS: 类似于tryComplete,但是在不需要调用每个‘子’任务的onCompletion方法时,
     * 可以使用此方法
     */
    public final void propagateCompletion() {
        CountedCompleter<?> a = this, s = a;
        for (int c;;) {
            if ((c = a.pending) == 0) {
                if ((a = (s = a).completer) == null) {
                    s.quietlyComplete();
                    return;
                }
            }
            else if (U.compareAndSwapInt(a, PENDING, c, c - 1))
                return;
        }
    }
 
    /**
     * 当获得多个子任务的任何一个(而不是所有)结果时,此方法可能非常有用。
     * 但是,在不覆盖 setRawResult 的常见(也是推荐的)情况下,
     * completeroot();可以更简单地获得这种效果。
     *  PS:适用于findAny这种场景
     *
     * @param rawResult the raw result
     */
    public void complete(T rawResult) {
        CountedCompleter<?> p;
        setRawResult(rawResult);
        onCompletion(this);
        quietlyComplete();
        if ((p = completer) != null)
            p.tryComplete();
    }
 
    /**
     * 如果此任务的挂起计数为零,则返回此任务;否则递减其挂起计数并返回  null。
     * 此方法被设计为在完成遍历循环中与 nextComplete一起使用。
     *
     * @return this task, if pending count was zero, else {@code null}
     */
    public final CountedCompleter<?> firstComplete() {
        for (int c;;) {
            if ((c = pending) == 0)
                return this;
            else if (U.compareAndSwapInt(this, PENDING, c, c - 1))
                return null;
        }
    }
 
    /**
     * 如果此任务没有完成器,则调用FJT#quiet complete并返回null
     * 或者,如果完成器的挂起计数非零,则减除该挂起计数并返回 null
     * 否则,返回完成器。
     * 此方法可用于同构任务层次结构的完成遍历循环的一部分:
     *
     * <pre> {@code
     * for (CountedCompleter<?> c = firstComplete();
     *      c != null;
     *      c = c.nextComplete()) {
     *   // ... process c ...
     * }}</pre>
     * PS: 简单来说,就是遍历查找完成的任务。
     * @return the completer, or {@code null} if none
     */
    public final CountedCompleter<?> nextComplete() {
        CountedCompleter<?> p;
        if ((p = completer) != null)
            return p.firstComplete();
        else {
            quietlyComplete();
            return null;
        }
    }
 
    /**
     * 等效于getRoot().quietlyComplete()
     * PS: 遍历到root的completer,执行其quietlyComplete方法
     */
    public final void quietlyCompleteRoot() {
        for (CountedCompleter<?> a = this, p;;) {
            if ((p = a.completer) == null) {
                a.quietlyComplete();
                return;
            }
            a = p;
        }
    }
 
    /**
     * 如果当前task尚未完成,则尝试去运行指定数量的未运行的完成路径上的任务
     * (如果已知存在该任务)
     *
     * @param maxTasks the maximum number of tasks to process.  If
     *                 less than or equal to zero, then no tasks are
     *                 processed.
     */
    public final void helpComplete(int maxTasks) {
        Thread t; ForkJoinWorkerThread wt;
        if (maxTasks > 0 && status >= 0) {
            if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread)
                (wt = (ForkJoinWorkerThread)t).pool.
                    helpComplete(wt.workQueue, this, maxTasks);
            else
                ForkJoinPool.common.externalHelpComplete(this, maxTasks);
        }
    }
 
    /**
     * 支持FJT的异常传播
     */
    void internalPropagateException(Throwable ex) {
        CountedCompleter<?> a = this, s = a;
        while (a.onExceptionalCompletion(ex, s) &&
               (a = (s = a).completer) != null && a.status >= 0 &&
               a.recordExceptionalCompletion(ex) == EXCEPTIONAL)
            ;
    }
 
    /**
     * 为CountedCompleters实现执行约定
     */
    protected final boolean exec() {
        compute();
        return false;
    }
 
    /**
     * 一般会覆盖重写实现
     *
     * @return the result of the computation
     */
    public T getRawResult() { return null; }
 
    /**
     * 包含结果的CountedCompleters可以选择使用的方法来帮助维护结果数据。
     * 默认情况下,什么也不做。不建议重写。但是,如果重写此方法以更新现有对
     * 象或字段,则通常必须将其定义为线程安全的.
     */
    protected void setRawResult(T t) { }
 
    // Unsafe mechanics
    private static final sun.misc.Unsafe U;
    private static final long PENDING;
    static {
        try {
            U = sun.misc.Unsafe.getUnsafe();
            PENDING = U.objectFieldOffset
                (CountedCompleter.class.getDeclaredField("pending"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }
}

示例一

 
  /**
  * 可以注意到,在递归情况下,任务在for它的右任务之后什么也不做,因此可以在返回之前直接调用它的左
  * 任务,从而改进这种设计。(这类似于尾部递归移除。)此外,由于任务在执行其左任务时返回(而不是通
  * 过调用{@code tryComplete}),挂起计数被设置为1:
  */
  class MyOperation<E> { void apply(E e) { ... }  }
 
  class ForEach<E> extends CountedCompleter<Void> {
 
    public static <E> void forEach(E[] array, MyOperation<E> op) {
      new ForEach<E>(null, array, op, 0, array.length).invoke();
    }
 
    final E[] array; final MyOperation<E> op; final int lo, hi;
    ForEach(CountedCompleter<?> p, E[] array, MyOperation<E> op, int lo, int hi) {
      super(p);
      this.array = array; this.op = op; this.lo = lo; this.hi = hi;
    }
 
    public void compute() { // version 1
      if (hi - lo >= 2) {
        int mid = (lo + hi) >>> 1;
        //PS: 这里手动设置等待的任务,实质上就是等待左右两个部分的任务进行fork
        setPendingCount(2); // must set pending count before fork
        new ForEach(this, array, op, mid, hi).fork(); // right child
        new ForEach(this, array, op, lo, mid).fork(); // left child
     }
      else if (hi > lo)
        op.apply(array[lo]);
      tryComplete();
    }
  }
 
//改进版本:
class ForEach<E> ...
    public void compute() { // version 2
       if (hi - lo >= 2) {
        int mid = (lo + hi) >>> 1;
        setPendingCount(1); // only one pending
        new ForEach(this, array, op, mid, hi).fork(); // right child
        new ForEach(this, array, op, lo, mid).compute(); // direct invoke
      }
      else {
        if (hi > lo)
          op.apply(array[lo]);
        tryComplete();
      }
    }
}</pre>

示例一再改进

/* 作为进一步的改进,请注意左边的任务甚至不需要存在。
* 我们可以使用原始任务进行迭代,并为每个fork添加一个挂起计数,而不是创建一个新任务。
* 此外,由于此树中没有任何任务实现{@link #onCompletion(CountedCompleter)}方法,
* 所以{@code tryComplete()}可以替换为{@link #propagateCompletion}。
*/
//PS: 这里通过循环实质上分解了 原来‘左’边的任务,而‘右’边的任务,则通过new ForEach自身
//的compute自行处理(即当成全部任务递归分解)。
class ForEach<E> ...
    public void compute() { // version 3
      int l = lo,  h = hi;
      while (h - l >= 2) {
        int mid = (l + h) >>> 1;
        addToPendingCount(1);
        new ForEach(this, array, op, mid, h).fork(); // right child
        h = mid;
      }
      if (h > l)
        op.apply(array[l]);
      propagateCompletion();
    }
}
 
//应用的一个搜索,在分段搜索中,找到一个,就结束。
class Searcher<E> extends CountedCompleter<E> {
    final E[] array; final AtomicReference<E> result; final int lo, hi;
    Searcher(CountedCompleter<?> p, E[] array, AtomicReference<E> result, int lo, int hi) {
      super(p);
      this.array = array; this.result = result; this.lo = lo; this.hi = hi;
    }
    public E getRawResult() { return result.get(); }
    public void compute() { // similar to ForEach version 3
      int l = lo,  h = hi;
      while (result.get() == null && h >= l) {
        if (h - l >= 2) {
          int mid = (l + h) >>> 1;
          addToPendingCount(1);
          new Searcher(this, array, result, mid, h).fork();
          h = mid;
        }
        else {
          E x = array[l];
          if (matches(x) && result.compareAndSet(null, x))
            quietlyCompleteRoot(); // root task is now joinable
          break;
        }
      }
      tryComplete(); // normally complete whether or not found
    }
    boolean matches(E e) { ... } // return true if found
 
    public static <E> E search(E[] array) {
        return new Searcher<E>(null, array, new AtomicReference<E>(), 0, array.length).invoke();
    }
}}

示例简化的MapReduce

//PS: 这种简化的MapReduce适用于无顺序要求的分解与归纳操作
//将所有别的节点当成兄弟节点;都可以直接进行归纳操作。
 class MyMapper<E> { E apply(E v) {  ...  } }
 class MyReducer<E> { E apply(E x, E y) {  ...  } }
class MapReducer<E> extends CountedCompleter<E> {
    final E[] array; final MyMapper<E> mapper;
    final MyReducer<E> reducer; final int lo, hi;
    MapReducer<E> sibling;
    E result;
    MapReducer(CountedCompleter<?> p, E[] array, MyMapper<E> mapper,
               MyReducer<E> reducer, int lo, int hi) {
      super(p);
      this.array = array; this.mapper = mapper;
      this.reducer = reducer; this.lo = lo; this.hi = hi;
    }
    public void compute() {
      if (hi - lo >= 2) {
        int mid = (lo + hi) >>> 1;
        MapReducer<E> left = new MapReducer(this, array, mapper, reducer, lo, mid);
        MapReducer<E> right = new MapReducer(this, array, mapper, reducer, mid, hi);
        left.sibling = right;
        right.sibling = left;
        setPendingCount(1); // only right is pending
        right.fork();
        left.compute();     // directly execute left
      }
      else {
        if (hi > lo)
            result = mapper.apply(array[lo]);
        tryComplete();
      }
    }
    public void onCompletion(CountedCompleter<?> caller) {
      if (caller != this) {
        MapReducer<E> child = (MapReducer<E>)caller;
        MapReducer<E> sib = child.sibling;
        if (sib == null || sib.result == null)
          result = child.result;
        else
          result = reducer.apply(child.result, sib.result);
      }
    }
    public E getRawResult() { return result; }
 
    public static <E> E mapReduce(E[] array, MyMapper<E> mapper, MyReducer<E> reducer) {
      return new MapReducer<E>(null, array, mapper, reducer,
                               0, array.length).invoke();
    }
}}
 
//PS: 改进版,通过next来连接MapReducer,使之有序。
 
class MapReducer<E> extends CountedCompleter<E> { // version 2
    final E[] array; final MyMapper<E> mapper;
    final MyReducer<E> reducer; final int lo, hi;
    MapReducer<E> forks, next; // record subtask forks in list
    E result;
    MapReducer(CountedCompleter<?> p, E[] array, MyMapper<E> mapper,
               MyReducer<E> reducer, int lo, int hi, MapReducer<E> next) {
      super(p);
      this.array = array; this.mapper = mapper;
      this.reducer = reducer; this.lo = lo; this.hi = hi;
      this.next = next;
    }
    public void compute() {
      int l = lo,  h = hi;
      while (h - l >= 2) {
        int mid = (l + h) >>> 1;
        addToPendingCount(1);
        (forks = new MapReducer(this, array, mapper, reducer, mid, h, forks)).fork();
        h = mid;
      }
      if (h > l)
        result = mapper.apply(array[l]);
      // process completions by reducing along and advancing subtask links
      for (CountedCompleter<?> c = firstComplete(); c != null; c = c.nextComplete()) {
        for (MapReducer t = (MapReducer)c, s = t.forks;  s != null; s = t.forks = s.next)
          t.result = reducer.apply(t.result, s.result);
      }
    }
    public E getRawResult() { return result; }
 
    public static <E> E mapReduce(E[] array, MyMapper<E> mapper, MyReducer<E> reducer) {
      return new MapReducer<E>(null, array, mapper, reducer,
                               0, array.length, null).invoke();
    }
}}

更多参考

ForkJoin框架之CountedCompleter

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
当然,我可以分享一些关于Spring Boot进阶笔记。请记住,以下笔记仅供参考,因为Spring Boot是一个非常广泛且不断发展的框架,有许多不同的用例和最佳实践。 1. 自定义Starter:Spring Boot提供了Starter的概念,它是一组必要的依赖项和配置的集合,可用于简化项目的配置。你可以通过创建自定义Starter来封装你的常用依赖项和配置,以便在多个项目中重复使用。 2. 运行时配置:Spring Boot提供了多种方式来配置应用程序。除了传统的application.properties或application.yml文件外,你还可以使用环境变量、命令行参数、系统属性等来配置应用程序。了解这些配置选项将帮助你更好地管理应用程序的行为。 3. 多环境支持:Spring Boot允许你为不同的环境(如开发、测试、生产)提供不同的配置。通过使用不同的配置文件或配置选项,你可以轻松地在不同的环境中管理应用程序的行为。 4. 自定义错误页面:Spring Boot提供了默认的错误页面,但你也可以自定义错误页面以提供更好的用户体验。通过创建一个自定义的错误处理器并将其注册到应用程序中,你可以捕获和处理特定类型的错误,并显示自定义的错误页面。 5. 安全性:Spring Boot集成了Spring Security框架,可以轻松地为应用程序添加身份验证和授权功能。你可以通过配置安全规则、自定义认证逻辑和使用各种身份验证提供者来保护你的应用程序。 6. 数据访问:Spring Boot简化了与各种数据源(如关系型数据库、NoSQL数据库、消息队列等)的集成。你可以使用Spring Data JPA、Spring Data MongoDB等模块来简化数据访问层的开发。 7. 缓存:Spring Boot提供了对各种缓存提供者(如Ehcache、Redis等)的集成支持。通过简单的配置,你可以启用缓存功能,并将其应用于适当的方法或查询。 这些只是Spring Boot进阶中的一些主题,还有许多其他方面可以深入研究。希望这些笔记能对你有所帮助!

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值