详解Java8中新引入的异步编程API之CompletableFuture

Java8中引入了不少新的特性,如:

  • lambda表达式:Lambda表达式允许在代码中直接定义匿名函数,使得Java对函数式编程的支持更加完善。它极大地简化了代码,使得开发者能够编写出更简洁、更灵活的代码。
  • Stream API:Stream API为处理数据提供了一种高效且易于使用的方式。通过使用Stream API,开发者可以编写出更简洁、更易于阅读的代码来处理集合数据。它提供了诸如filter、map、sorted等常用的方法,使得数据操作更加直观和方便。
  • Date/Time API:Java 8引入了新的日期和时间API,替代了之前复杂且易出错的java.util.Date和java.util.Calendar类。新的API提供了更加直观和灵活的方式来处理日期和时间。
  • 接口的默认方法和静态方法:在Java 8中,接口可以包含默认方法和静态方法。默认方法允许在接口中定义方法的具体实现,而静态方法与类中的静态方法类似,可以在接口中使用static关键字定义。
  • 函数式接口:Java 8引入了函数式接口的概念,这是一个只包含一个抽象方法的接口。Lambda表达式可以很容易地与函数式接口结合使用,从而支持函数式编程。
  • 方法引用:方法引用是Lambda表达式的一个简化形式,它允许我们直接引用已存在的方法或构造器。
  • Optional容器:Optional是一个可以为null的容器对象。它的引入旨在减少空指针异常的发生,使得代码更加健壮。
  • CompletableFuture

今天,我们就来看看这个CompletableFuture,它是什么、怎么用、以及使用过程中需要注意什么。

是什么?(CompletableFuture,Future,CompletionStage,Function,Consumer,Supplier...)

        CompletableFuture是Java 8中引入的一个功能强大的类,它位于包JUC(java.util.concurrent)下,是Java Future和CompletionStage API的扩展,专为异步编程而设计。它允许我们将任务运行在与主线程分离的其他线程中,并通过回调在主线程中得到异步任务执行的状态,包括是否完成、是否异常等信息。这种设计使得主线程不会阻塞或等待任务的完成,从而可以并行执行其他任务,极大地提高了程序的性能。

        CompletableFuture的主要用途在于异步编程,特别是在需要执行耗时的操作(如I/O操作)时,可以将其放在后台线程中执行,同时主线程可以继续执行其他任务。一旦后台任务完成,主线程可以通过回调机制获取结果或处理异常情况(基于观察者设计模式)。

/**
 * A {@link Future} that may be explicitly completed (setting its
 * value and status), and may be used as a {@link CompletionStage},
 * supporting dependent functions and actions that trigger upon its
 * completion.
 *
 * <p>When two or more threads attempt to
 * {@link #complete complete},
 * {@link #completeExceptionally completeExceptionally}, or
 * {@link #cancel cancel}
 * a CompletableFuture, only one of them succeeds.
 *
 * <p>In addition to these and related methods for directly
 * manipulating status and results, CompletableFuture implements
 * interface {@link CompletionStage} with the following policies: <ul>
 *
 * <li>Actions supplied for dependent completions of
 * <em>non-async</em> methods may be performed by the thread that
 * completes the current CompletableFuture, or by any other caller of
 * a completion method.</li>
 *
 * <li>All <em>async</em> methods without an explicit Executor
 * argument are performed using the {@link ForkJoinPool#commonPool()}
 * (unless it does not support a parallelism level of at least two, in
 * which case, a new Thread is created to run each task).  To simplify
 * monitoring, debugging, and tracking, all generated asynchronous
 * tasks are instances of the marker interface {@link
 * AsynchronousCompletionTask}. </li>
 *
 * <li>All CompletionStage methods are implemented independently of
 * other public methods, so the behavior of one method is not impacted
 * by overrides of others in subclasses.  </li> </ul>
 *
 * <p>CompletableFuture also implements {@link Future} with the following
 * policies: <ul>
 *
 * <li>Since (unlike {@link FutureTask}) this class has no direct
 * control over the computation that causes it to be completed,
 * cancellation is treated as just another form of exceptional
 * completion.  Method {@link #cancel cancel} has the same effect as
 * {@code completeExceptionally(new CancellationException())}. Method
 * {@link #isCompletedExceptionally} can be used to determine if a
 * CompletableFuture completed in any exceptional fashion.</li>
 *
 * <li>In case of exceptional completion with a CompletionException,
 * methods {@link #get()} and {@link #get(long, TimeUnit)} throw an
 * {@link ExecutionException} with the same cause as held in the
 * corresponding CompletionException.  To simplify usage in most
 * contexts, this class also defines methods {@link #join()} and
 * {@link #getNow} that instead throw the CompletionException directly
 * in these cases.</li> </ul>
 *
 * @author Doug Lea
 * @since 1.8
 */
public class CompletableFuture<T> implements Future<T>, CompletionStage<T>

 

从定义中,我们可以看到,CompletableFuture同时实现了Future接口和CompletionStage

Future接口中定义了如下几个方法:

如:通过get()方法可以获取异步任务的结果,isDone()可以判断任务是否已完成。

CompletionStage接口在Java中代表一个异步计算的可能阶段。它允许开发者定义当一个或多个CompletionStage完成时要执行的操作或计算的值。这个接口特别强大,因为它提供了多种方式来组合和编排异步任务。

CompletionStage接口内部的方法主要由几个关键单词组合而成,如applyacceptrunthenbotheitherasync。每个关键字都对应着一种特定的执行方式或用途:

  1. apply/combine:这些方法用于在上一阶段执行结束之后,将上一阶段的结果作为指定函数的参数执行函数以产生新的结果。接口参数为BiFunctionFunction类型。
  2. accept:这些方法用于在上一阶段执行结束之后,将上一阶段的结果作为指定操作的参数执行操作,但不会对阶段结果产生影响。接口参数为BiConsumerConsumer类型。
  3. run:这些方法定义的操作不依赖于上一阶段的执行结果。只要上一阶段完成(但一般要求正常完成),就会执行指定的操作,且不会对阶段的结果产生影响。接口参数为Runnable类型。
  4. then:这些方法通常用于安排对单个阶段的依赖,并定义当该阶段完成后要执行的操作
  5. both/either:这些方法用于处理两个阶段的完成情况。both方法会在两个阶段都完成时执行操作,而either方法则会在任一阶段完成时执行操作
  6. async:这个方法关键字表示异步执行可以在一个ForkJoinPool线程池里面,以守护进程的形式执行,或者传入一个Executor作为任务执行的线程池

CompletableFutureCompletionStage接口的一个实现,它扩展了Future接口并增加了异步会点、流式处理以及多个Future组合处理的能力。这使得Java在处理多任务的协同工作时更加顺畅和便利。 

这里,我们再多了解一点BiFunction/Function,BiConsumer/Consumer,Supplier(因为后面的各种方法几乎都会用到相关类型的参数,所以有必要了解一下)

Function:一个函数式接口,R apply(T t)方法,接收一个参数,返回一个结果;


/**
 * Represents a function that accepts one argument and produces a result.
 *
 * <p>This is a <a href="package-summary.html">functional interface</a>
 * whose functional method is {@link #apply(Object)}.
 *
 * @param <T> the type of the input to the function
 * @param <R> the type of the result of the function
 *
 * @since 1.8
 */
@FunctionalInterface
public interface Function<T, R> {

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);

    /**
     * Returns a composed function that first applies the {@code before}
     * function to its input, and then applies this function to the result.
     * If evaluation of either function throws an exception, it is relayed to
     * the caller of the composed function.
     *
     * @param <V> the type of input to the {@code before} function, and to the
     *           composed function
     * @param before the function to apply before this function is applied
     * @return a composed function that first applies the {@code before}
     * function and then applies this function
     * @throws NullPointerException if before is null
     *
     * @see #andThen(Function)
     */
    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }

    /**
     * Returns a composed function that first applies this function to
     * its input, and then applies the {@code after} function to the result.
     * If evaluation of either function throws an exception, it is relayed to
     * the caller of the composed function.
     *
     * @param <V> the type of output of the {@code after} function, and of the
     *           composed function
     * @param after the function to apply after this function is applied
     * @return a composed function that first applies this function and then
     * applies the {@code after} function
     * @throws NullPointerException if after is null
     *
     * @see #compose(Function)
     */
    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }

    /**
     * Returns a function that always returns its input argument.
     *
     * @param <T> the type of the input and output objects to the function
     * @return a function that always returns its input argument
     */
    static <T> Function<T, T> identity() {
        return t -> t;
    }
}

BiFunction:接收两个参数,返回一个结果,是Function的二元化。

/**
 * Represents a function that accepts two arguments and produces a result.
 * This is the two-arity specialization of {@link Function}.
 *
 * <p>This is a <a href="package-summary.html">functional interface</a>
 * whose functional method is {@link #apply(Object, Object)}.
 *
 * @param <T> the type of the first argument to the function
 * @param <U> the type of the second argument to the function
 * @param <R> the type of the result of the function
 *
 * @see Function
 * @since 1.8
 */
@FunctionalInterface
public interface BiFunction<T, U, R> {

    /**
     * Applies this function to the given arguments.
     *
     * @param t the first function argument
     * @param u the second function argument
     * @return the function result
     */
    R apply(T t, U u);

    /**
     * Returns a composed function that first applies this function to
     * its input, and then applies the {@code after} function to the result.
     * If evaluation of either function throws an exception, it is relayed to
     * the caller of the composed function.
     *
     * @param <V> the type of output of the {@code after} function, and of the
     *           composed function
     * @param after the function to apply after this function is applied
     * @return a composed function that first applies this function and then
     * applies the {@code after} function
     * @throws NullPointerException if after is null
     */
    default <V> BiFunction<T, U, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t, U u) -> after.apply(apply(t, u));
    }
}

Consumer:接收一个参数,无返回值。

/**
 * Represents an operation that accepts a single input argument and returns no
 * result. Unlike most other functional interfaces, {@code Consumer} is expected
 * to operate via side-effects.
 *
 * <p>This is a <a href="package-summary.html">functional interface</a>
 * whose functional method is {@link #accept(Object)}.
 *
 * @param <T> the type of the input to the operation
 *
 * @since 1.8
 */
@FunctionalInterface
public interface Consumer<T> {

    /**
     * Performs this operation on the given argument.
     *
     * @param t the input argument
     */
    void accept(T t);

    /**
     * Returns a composed {@code Consumer} that performs, in sequence, this
     * operation followed by the {@code after} operation. If performing either
     * operation throws an exception, it is relayed to the caller of the
     * composed operation.  If performing this operation throws an exception,
     * the {@code after} operation will not be performed.
     *
     * @param after the operation to perform after this operation
     * @return a composed {@code Consumer} that performs in sequence this
     * operation followed by the {@code after} operation
     * @throws NullPointerException if {@code after} is null
     */
    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

BiConsumer:接收两个参数,无返回值

/**
 * Represents an operation that accepts two input arguments and returns no
 * result.  This is the two-arity specialization of {@link Consumer}.
 * Unlike most other functional interfaces, {@code BiConsumer} is expected
 * to operate via side-effects.
 *
 * <p>This is a <a href="package-summary.html">functional interface</a>
 * whose functional method is {@link #accept(Object, Object)}.
 *
 * @param <T> the type of the first argument to the operation
 * @param <U> the type of the second argument to the operation
 *
 * @see Consumer
 * @since 1.8
 */
@FunctionalInterface
public interface BiConsumer<T, U> {

    /**
     * Performs this operation on the given arguments.
     *
     * @param t the first input argument
     * @param u the second input argument
     */
    void accept(T t, U u);

    /**
     * Returns a composed {@code BiConsumer} that performs, in sequence, this
     * operation followed by the {@code after} operation. If performing either
     * operation throws an exception, it is relayed to the caller of the
     * composed operation.  If performing this operation throws an exception,
     * the {@code after} operation will not be performed.
     *
     * @param after the operation to perform after this operation
     * @return a composed {@code BiConsumer} that performs in sequence this
     * operation followed by the {@code after} operation
     * @throws NullPointerException if {@code after} is null
     */
    default BiConsumer<T, U> andThen(BiConsumer<? super T, ? super U> after) {
        Objects.requireNonNull(after);

        return (l, r) -> {
            accept(l, r);
            after.accept(l, r);
        };
    }
}

Supplier:无参数,有返回值。

/**
 * Represents a supplier of results.
 *
 * <p>There is no requirement that a new or distinct result be returned each
 * time the supplier is invoked.
 *
 * <p>This is a <a href="package-summary.html">functional interface</a>
 * whose functional method is {@link #get()}.
 *
 * @param <T> the type of results supplied by this supplier
 *
 * @since 1.8
 */
@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

当然,还有比较熟悉的Runnable:无参数、无返回值。

/**
 * The <code>Runnable</code> interface should be implemented by any
 * class whose instances are intended to be executed by a thread. The
 * class must define a method of no arguments called <code>run</code>.
 * <p>
 * This interface is designed to provide a common protocol for objects that
 * wish to execute code while they are active. For example,
 * <code>Runnable</code> is implemented by class <code>Thread</code>.
 * Being active simply means that a thread has been started and has not
 * yet been stopped.
 * <p>
 * In addition, <code>Runnable</code> provides the means for a class to be
 * active while not subclassing <code>Thread</code>. A class that implements
 * <code>Runnable</code> can run without subclassing <code>Thread</code>
 * by instantiating a <code>Thread</code> instance and passing itself in
 * as the target.  In most cases, the <code>Runnable</code> interface should
 * be used if you are only planning to override the <code>run()</code>
 * method and no other <code>Thread</code> methods.
 * This is important because classes should not be subclassed
 * unless the programmer intends on modifying or enhancing the fundamental
 * behavior of the class.
 *
 * @author  Arthur van Hoff
 * @see     java.lang.Thread
 * @see     java.util.concurrent.Callable
 * @since   JDK1.0
 */
@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

好了,了解了这些基本概念之后,我们接下来看看CompletableFuture怎么用。

怎么用?

 我们先来直观感受下,CompletableFuture提供了哪些方法

 

看到这么多的方法,很多很多,眼花缭乱,有没有?

不慌,先来总结一下,总体上有个概念,再做展开。 

 首先,它肯定提供了创建异步任务的方法,方便我们去快捷的创建一个新的异步任务,这种类型的方法要么是构造方法,要么就是static方法

其次,它实现了CompletionStage,而CompletionStage中的方法又有几个主要的关键字(run,accept,apply,then,both,async,combine),而这几个关键词又可以按用途分为异步回调类、组合处理类、还有辅助处理类等等这几种类型

再者,我们结合Funcion/Consumer/Supplier定义,再观察CompletableFuture中的方法,不难看出,*supply*方法参数为Supplier类型:表示无参数、有返回值,*accept*方法参数为Consumer类型:表示有参数、无返回值,*apply*方法参数为Function类型:表示有参数、有返回值,而*run*方法参数为Runnable类型:表示无参数、无返回值。那么,有了这个整体模式上的概念,用起来就更方便了。

还有,很多方法都有一个加了Async,也就是*Async的方法,以及Async方法还有一个多了个Executor类型参数的重载方法,而Async我们从字面意思便可理解为它是和异步有关的,那么,不带Executor类型参数的自然是用了默认的线程池,而带有Executor类型参数的便是指定线程池执行

All async methods without an explicit Executor argument are performed using the ForkJoinPool.commonPool()
 (unless it does not support a parallelism level of at least two, in which case, a new Thread is created to run each task).
 To simplify monitoring, debugging, and tracking,
 all generated asynchronous tasks are instances of the marker interface CompletableFuture.AsynchronousCompletionTask.

        关于默认线程池,在CompletableFuture的javadoc中是这么说明的,在当硬件支持并发的条件下,默认使用的是FockJoinPool.commonPool()公共线程池,而如果硬件本身不支持并发(单核cpu),那么每个任务都会创建一个新线程执行。

好了,有了上面的总结和认识,我们再来从头来看CompletableFuture中提供的方法,主要分如下几类:(也正因为有了上面的总结,所以Async方法,以及带有Executor类型参数的重载方法就不做过多介绍了,再巩固一下,统一理解为:*Async方法,执行任务的并不一定是同一个线程,会用到线程池,而线程池默认为ForkJoinPool.commonPool(),也支持自定义

创建异步任务:

  • supplyAsync:接收一个Supplier函数式接口作为参数,异步执行该函数,并返回表示异步计算结果的CompletableFuture
  • runAsync:接收一个Runnable作为参数,异步执行该任务,并返回一个表示异步操作完成通知的CompletableFuture(无返回值)。

异步回调:

  • thenApply:当前任务完成后,执行一个函数并将结果传递给新的CompletableFuture
  • thenAccept:当前任务完成后,执行一个接受结果的消费操作,并返回表示该操作的CompletableFuture(无返回值)。
  • thenRun:当前任务完成后,执行一个无参数的Runnable任务,并返回表示该操作的CompletableFuture(无返回值)。
  • exceptionally:处理当前CompletableFuture计算过程中发生的异常,并返回一个新的CompletableFuture
  • whenComplete:无论正常完成还是异常完成,都会执行指定的动作。
  • handle:与whenComplete类似,但允许你根据完成情况返回一个新的结果或抛出异常。

 组合处理:

  • thenCompose:当前任务完成后,执行一个返回CompletableFuture的函数,并返回该函数的结果。
  • thenCombine:等待两个CompletableFuture都完成,然后应用一个函数将两个结果组合成一个新结果。
  • thenAcceptBoth:等待两个CompletableFuture都完成,然后应用一个接受两个结果的消费操作。
  • runAfterBoth:等待两个CompletableFuture都完成,然后执行一个Runnable任务。
  • applyToEither:等待两个CompletableFuture中的任一个完成,然后应用一个函数到该结果。
  • acceptEither:等待两个CompletableFuture中的任一个完成,然后应用一个接受结果的消费操作。
  • runAfterEither:等待两个CompletableFuture中的任一个完成,然后执行一个Runnable任务。
  • allOf/anyOf:allOf返回的CompletableFuture是多个任务都执行完成后才会执行,只要有一个任务执行异常,则返回的CompletableFuture执行get方法时会抛出异常,如果都是正常执行,则get返回null。

 代码示例:

以下是一个简单的使用示例,展示CompletableFuture的几种方法:

import java.util.concurrent.CompletableFuture;  
import java.util.concurrent.ExecutionException;  
  
public class CompletableFutureDemo {  
    public static void main(String[] args) throws ExecutionException, InterruptedException {  
        // 创建异步任务  
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {  
            try {  
                Thread.sleep(1000); // 模拟耗时任务  
            } catch (InterruptedException e) {  
                throw new IllegalStateException(e);  
            }  
            return "Hello, World!";  
        });  
  
        // 异步回调  
        future.thenAccept(System.out::println);  
        future.thenRun(() -> System.out.println("Task completed!"));  
  
        // 组合处理  
        CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> 10);  
        CompletableFuture<Integer> future3 = CompletableFuture.supplyAsync(() -> 20);  
        CompletableFuture<Integer> sumFuture = future2.thenCombine(future3, (a, b) -> a + b);  
        System.out.println("Sum: " + sumFuture.get());  
  
        // 取消任务(如果尚未开始)  
        // future.cancel(true);  
    }  
}

异常处理:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {  
    throw new RuntimeException("Error during computation");  
});  
  
CompletableFuture<String> exceptionHandledFuture = future.exceptionally(e -> {  
    System.err.println("Caught exception: " + e.getMessage());  
    return "Default value";  
});  
  
String result = exceptionHandledFuture.get();  
System.out.println(result); // 输出:Default value

allOf:

// 创建异步执行任务:
        CompletableFuture<Double> cf = CompletableFuture.supplyAsync(()->{
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
            return 1.0;
        });
        CompletableFuture<Double> cf2 = CompletableFuture.supplyAsync(()->{
            try {
                Thread.sleep(1500);
            } catch (InterruptedException e) {
            }
            return 2.0;
        });
        CompletableFuture<Double> cf3 = CompletableFuture.supplyAsync(()->{
            try {
                Thread.sleep(1300);
            } catch (InterruptedException e) {
            }

            return 3.0;
        });
        //allof等待所有任务执行完成才执行,如果有一个任务异常终止,则cf4.get时会抛出异常,如果都正常执行,cf4.get返回null
        //anyOf是只要有一个任务执行完成,无论是正常执行或者执行异常,都会执行cf4,cf4.get的结果就是已执行完成的任务的执行结果
        CompletableFuture cf4=CompletableFuture.allOf(cf,cf2,cf3).whenComplete((a,b)->{
           if(b!=null){
               System.out.println("error stack trace->");
               b.printStackTrace();
           }else{
               System.out.println("run succ,result->"+a);
           }
        });
 
        //等待任务执行完成
        System.out.println("cf4 run result->"+cf4.get());

 以及普通的join方法:(别忘了,它还实现了Future接口)

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {  
    // 模拟耗时任务  
    try {  
        Thread.sleep(1000);  
    } catch (InterruptedException e) {  
        throw new IllegalStateException(e);  
    }  
    return "Result of asynchronous computation";  
});  
  
// 使用 join 方法获取结果,如果结果还未准备好,会阻塞当前线程  
String result = future.join(); // 阻塞等待异步任务完成,然后返回结果  
System.out.println(result); // 输出:Result of asynchronous computation

现在,知道是CompletableFuture是什么,也知道了它怎么用了。

最后,再看看使用时需要注意什么吧。

使用CompletableFuture需要注意的地方

  1. 线程池的选择:默认情况下,CompletableFuture会使用公共的ForkJoinPool线程池。但是,如果所有CompletableFuture共享一个线程池,并且某些任务执行较慢的I/O操作,可能会导致线程池中所有线程都阻塞在I/O操作上,从而造成线程饥饿。因此,建议根据不同的业务类型创建不同的线程池。
  2. 异常处理:在使用CompletableFuture时,需要注意异常处理。如果在异步任务中抛出异常,而后续操作没有正确处理这些异常,可能会导致程序出现不可预料的行为。例如,前面提到的代码片段中,如果在supplyAsync中抛出的异常没有被正确传播到后续操作中,那么调用result.join()时可能不会抛出预期的异常。
  3. 结果获取:虽然CompletableFuture提供了非阻塞的方式来获取异步任务的结果(如通过回调函数),但在某些情况下,你可能需要阻塞等待结果。这时,可以使用CompletableFuture的get方法。但请注意,get方法会阻塞调用线程,直到异步任务完成或超时。

总的来说,CompletableFuture为Java的异步编程提供了强大的支持,但使用时也需要注意线程池的选择、异常处理以及结果获取等方面的问题。

好了,有了这些,基本也够应用日常工作使用了,多学习、多练习、多使用、多踩坑,才能不断提升、进步。

最后的最后,建议学习一个框架,vertx,有了CompletaleFuture基础,学习起来应该会更容易一些,可以试试看,多交流。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值