Executor多线程框架学习笔记(三):ExecutorService

目录

ExecutorService

ScheduleExecutorService

ScheduledFuture

 AbstractExecutorService


ExecutorService

ExecutorService是一个服务可以当做管理工具,他管理了执行器的创建和停止,既然是执行器服务那么就代表着他是可以管理多个执行器的否则也就没有意义,但是也是一个接口对于他的定义如下。

 

//停止执行器,虽然停止但是会将在停止前已经存在的任务将会尝试停止如果失败则继续执行完成为止。
void shutdown();
//停止执行器,相比上方的停止这个就暴力一些,立马停止并且取消停止前已经存在的任务并且尝试关闭正在执行的任务。
List<Runnable> shutdownNow();
//执行器服务是否停止服务(由子类实现决定)
boolean isShutdown();
//该方法是对shutdown或shutdownNow的状态获取是否终止完成。如果没有调用这两个方法那么此方法永远是false(具体情况有子类决定)
boolean isTerminated();
//等待终止服务,此方法不会有任何的操作仅仅是等待任务的执行,会有两个结果1、要么执行结束2、要么执行时间超过操作时间
boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;
//提交一个可以带返回值的任务(下方会详细介绍Future这个类)
//Callable只有一个方法 V call(),Future会默认将他的返回存到自己的成员变量中返回,调用get方法获取
<T> Future<T> submit(Callable<T> task);
//提交一个Runnable任务会默认的将传入的result存入到一个Future中在进行返回
<T> Future<T> submit(Runnable task, T result);
//提交一个Runnable任务会默认将null存入Future的成员变量中通过get获得
Future<?> submit(Runnable task);
//传入一个Callable类型集合并且等到集合处理完成一起返回,最终返回一个对应结果的Future集合。
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException;
//相比上方此方法设置了超时时间,等待任务的超时,如果超时则进行任务的取消处理,继续会放回对应的结果列表但是如果是被取消的结果则会抛出取消异常,这代表着如果在不缺点是否成功的时候需要调用isCancelled方法此方法校验当前的Future是否取消如果取消了则不要调用get因为会出异常。
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,long timeout, TimeUnit unit)throws InterruptedException;
//传入指定的任务集合只要其中任何一个任务完成则取消其他任务并且返回完成的任务的返回值,这里并不是Future而是Callable的返回类型
<T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException;
//传入指定的任务集合只要启动任何一个任务完成则取消其他的任务执行并且返回第一执行完成的任务结果,如果连第一个任务执行都超时了那么不会返回结果会直接抛出TimeOut的超时异常。
<T> T invokeAny(Collection<? extends Callable<T>> tasks,long timeout, TimeUnit unit)throws InterruptedException, ExecutionException, TimeoutException;

ExecutorService的定义到此结束了。接下来讲述他依赖的类型Future和Callable。

//这个接口非常简单定义一个call方法此方法只有两个结果要么抛出业务异常,要么返回计算的结果,而结果类型则是创建时设置的泛型V。
public interface Callable<V> {
    V call() throws Exception;
}
public interface Future<V> {
    //尝试取消任务执行,如果取消失败则可能是任务已完成或者已经取消或其他原因导致失败,如果成功分为两种情况1、任务未运行则永远不会执行2、任务已经执行mayInterruptIfRunning需要判断他是否为true如果是则尝试中断执行。
    boolean cancel(boolean mayInterruptIfRunning);
    //如果是主动取消的则返回true,否则任何形式的结束此方法都是false
    boolean isCancelled();
    //在任何情况下都会返回true,除非是新new的调用此方法是false具体看子类状态的实现。
    boolean isDone();
    //等待计算完成获取结果,有等待就代表是阻塞的。如果执行异常则会抛出
    V get() throws InterruptedException, ExecutionException;
    //等待指定的时间获取结果如果超时则抛出TimeoutException,如果执行异常则会抛出
    V get(long timeout, TimeUnit unit)throws InterruptedException, ExecutionException,TimeoutException;
}

ScheduleExecutorService

ScheduleExecutorService是一个支持周期调度的线程池,我们可以设置调度的周期period,ScheduleExecutorService会按照设定好的周期调度我们的任务。

来看一下他的方法列表:

//可以看出他继承与ExecutorService,并且对ExecutorService的结构进行了扩展
public interface ScheduledExecutorService extends ExecutorService {
    //创建一个延迟执行的任务,此任务延迟时间有delay与unit控制,返回值Future的get方法返回null,因为run方法并没有返回值所以返回默认值
    public ScheduledFuture<?> schedule(Runnable command,long delay, TimeUnit unit);
    //是上方方法的重构,将Runnable改为Callable,因为传入的试Callable所以在Future的get方法返回call方法的返回值,看不明白的读者可以根据上方的Callable的讲解进行理解。
    public <V> ScheduledFuture<V> schedule(Callable<V> callable,long delay, TimeUnit unit);     
    //之前创建的延迟启动的任务都只执行一次,此方法是根据initialDelay为运行起点等待设置的启动时长,然后以period为周期的循环执行任务。除非取消任务或者中断调用者线程否则次任务永久运行,因为是永久运行所以无返回值,默认都为NULL。
    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit);
    //scheduleAtFixedRate的启动一样都是指定initialDelay的时长后运行,也是在指定的周期中重复运行,除非遇到异常或线程中断和取消执行否则将永久运行。
    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit);
    //那么scheduleAtFixedRate和scheduleWithFixedDelay他们的区别是什么?
    //scheduleWithFixedDelay:在计算周期的时候会将任务的完成时间算进去,意思就是如果任务执行了1s而周期是2s那么下次执行的时间会是启动时间+3s。
    //对应公式:下一次执行开始时间=上一次执行结束时间+周期+上一次执行的时长
    //scheduleAtFixedRate:恰恰相反,计算周期的时候不会管你的执行时间会继续按照设置的周期计算,比如任务执行了1s而周期是2s那么就会根据你的第一次启动时间+2s。
    //对应公式:下一次执行开始时间=上次执行结束时间+周期
    //这里会有一个疑问,那就是如果我周期设置为2s而任务执行了3s,scheduleAtFixedRate方法不是不计操作时长的吗?那么会不会出现第一次执行还未结束第二次已经开始呢?这个问题是不存在的,因为在前面讲解的时候就说了结束周期任务中有一条就是当线程中断就会结束周期,意思就是一个线程维护一个周期任务,一旦线程中断那么任务也就结束了,如果多个线程对应一个任务的时候只有一个线程在运行任务其他线程都在等待状态。
}

先给个例子:


    private static Runnable blockRunner = () -> {
        try {
            TimeUnit.SECONDS.sleep(2);
            System.out.println("one round:" + new Date());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    };

    private static ScheduledExecutorService scheduledExecutorService =
            Executors.newScheduledThreadPool(2);

    public static void main(String ... args) {


        scheduledExecutorService
                .scheduleAtFixedRate(blockRunner, 0, 100, TimeUnit.MILLISECONDS);

    }

我们设定了调度周期为100毫秒,但是blockRunner实际上需要执行2秒才能返回。

先来看一下scheduleAtFixedRate这个方法:


    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit) {
        if (command == null || unit == null)
            throw new NullPointerException();
        if (period <= 0)
            throw new IllegalArgumentException();
        ScheduledFutureTask<Void> sft =
            new ScheduledFutureTask<Void>(command,
                                          null,
                                          triggerTime(initialDelay, unit),
                                          unit.toNanos(period));
        RunnableScheduledFuture<Void> t = decorateTask(command, sft);
        sft.outerTask = t;
        delayedExecute(t);
        return t;
    }

我们的任务command被包装了两次,一次变成了一个ScheduledFutureTask类型的对象,然后又变成了RunnableScheduledFuture类型的对象。然后执行了一个方法delayedExecute,这个方法字面意思上看起来像是延时执行的意思,看一下它的代码:


    private void delayedExecute(RunnableScheduledFuture<?> task) {
        if (isShutdown())
            reject(task);
        else {
            super.getQueue().add(task);
            if (isShutdown() &&
                !canRunInCurrentRunState(task.isPeriodic()) &&
                remove(task))
                task.cancel(false);
            else
                ensurePrestart();
        }
    }

它的执行逻辑是:如果线程池被关闭了,那么拒绝提交的任务,否则,将该任务添加队列中去。这个队列就是ThreadPoolExecutor中的workQueue,而这个workQueue是在ThreadPoolExecutor的构造函数中被初始化的,也就是下面这关键的一句:

   public ScheduledThreadPoolExecutor(int corePoolSize,
                                       ThreadFactory threadFactory,
                                       RejectedExecutionHandler handler) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue(), threadFactory, handler);
    }

也就是说,我们的任务被添加到了一个DelayedWorkQueue队列中去了,它是一个可以延迟消费的阻塞队列。而延时的时间是通过接口Delayed的getDelay方法来获得的,我们最后找到ScheduledFutureTask实现了Delayed的getDelay方法。

   public long getDelay(TimeUnit unit) {
            return unit.convert(time - now(), NANOSECONDS);
        }

time变量是什么?原来是delay,好像和period无关啊!!分析了这么久,发现这是第一次执行任务的逻辑啊,我想知道的是第二次、第三次以后和初始的delay无关之后的周期调度的情况啊,继续找吧!

然后发现了ScheduledFutureTask的run方法,很明显这就是任务调度被执行的关键所在,看下代码:

        public void run() {
            boolean periodic = isPeriodic();
            if (!canRunInCurrentRunState(periodic))
                cancel(false);
            else if (!periodic)
                ScheduledFutureTask.super.run();
            else if (ScheduledFutureTask.super.runAndReset()) {
                setNextRunTime();
                reExecutePeriodic(outerTask);
            }
        }

最为关键的地方在于:


            else if (ScheduledFutureTask.super.runAndReset()) {
                setNextRunTime();
                reExecutePeriodic(outerTask);
            }

首先是:runAndReset()这个方法,然后是setNextRunTime()这个方法,然后是reExecutePeriodic(outerTask)这个方法。
第一个方法runAndReset()貌似是执行我们的提交的任务的,我们看下代码:


    protected boolean runAndReset() {
        if (state != NEW ||
            !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                         null, Thread.currentThread()))
            return false;
        boolean ran = false;
        int s = state;
        try {
            Callable<V> c = callable;
            if (c != null && s == NEW) {
                try {
                    c.call(); // don't set result
                    ran = true;
                } catch (Throwable ex) {
                    setException(ex);
                }
            }
        } finally {
            // runner must be non-null until state is settled to
            // prevent concurrent calls to run()
            runner = null;
            // state must be re-read after nulling runner to prevent
            // leaked interrupts
            s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
        return ran && s == NEW;
    }

关键的地方是c.call()这一句,这个c就是我们提交的任务。
第二个方法setNextRunTime()的意思是设置下次执行的时间,下面是他的代码细节:


        private void setNextRunTime() {
            long p = period;
            if (p > 0)
                time += p;
            else
                time = triggerTime(-p);
        }

我们只需要看p>0这个分支就可以了,其实这是两种策略。我们的示例对应了第一个分支的策略,所以很显然,time这个变量会被加p,而p则是我们设定好的period。下面我们找一下这个time是在哪里初始化的,回忆一下scheduleAtFixedRate这个方法的内,我们说我们的任务被包装了两次,而time就是在这里被初始化的:


    /**
     * Returns the trigger time of a delayed action.
     */
    private long triggerTime(long delay, TimeUnit unit) {
        return triggerTime(unit.toNanos((delay < 0) ? 0 : delay));
    }

    /**
     * Returns the trigger time of a delayed action.
     */
    long triggerTime(long delay) {
        return now() +
            ((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay));
    }

无论如何,我们知道一个任务会被运行完一次之后再次设置时间,然后线程池会获取任务来执行,而任务队列是一个延时阻塞队列,所以也就造成了周期性运行的假象。可以看下下面获取任务的take方法:


  public RunnableScheduledFuture<?> take() throws InterruptedException {
            final ReentrantLock lock = this.lock;
            lock.lockInterruptibly();
            try {
                for (;;) {
                    RunnableScheduledFuture<?> first = queue[0];
                    if (first == null)
                        available.await();
                    else {
                        long delay = first.getDelay(NANOSECONDS);
                        if (delay <= 0)
                            return finishPoll(first);
                        first = null; // don't retain ref while waiting
                        if (leader != null)
                            available.await();
                        else {
                            Thread thisThread = Thread.currentThread();
                            leader = thisThread;
                            try {
                                available.awaitNanos(delay);
                            } finally {
                                if (leader == thisThread)
                                    leader = null;
                            }
                        }
                    }
                }
            } finally {
                if (leader == null && queue[0] != null)
                    available.signal();
                lock.unlock();
            }
        }

可以看到,如果delay小于等于0,那么就是说需要被立即调度,否则延时delay这样一段时间。也就是延时消费。

结论就是,一个任务会被重复添加到一个延时任务队列,所以同一时间任务队列中会有多个任务待调度,线程池会首先获取优先级高的任务执行。如果我们的任务运行时间大于设置的调度时间,那么效果就是任务运行多长时间,调度时间就会变为多久,因为添加到任务队列的任务的延时时间每次都是负数,所以会被立刻执行。

ScheduledFuture

//在ScheduledExecutorService中依赖了ScheduledFuture,而从下方可以看出ScheduledFuture是一个空的接口他继承与Delayed和Future
public interface ScheduledFuture<V> extends Delayed, Future<V> {
}
//Delayed接口只定义了getDelay方法,此方法用于获取剩余的时长:getDelay=下次执行开始时间-当前时间。
public interface Delayed extends Comparable<Delayed> {
    long getDelay(TimeUnit unit);
}
//可以看出Delayed类继承了Comparable,此接口是用来比较两个对象的具体看子类实现,这里说下他的返回值,当前的数比较结果:-1小于、0等于、1大于。
public interface Comparable<T> {
    public int compareTo(T o);
}

 AbstractExecutorService

//此类的定义并没有特殊的意义仅仅是实现了ExecutorService接口
//而ExecutorService接口的定义在netty结构第一篇有讲述如果不清楚的读者可以去查看一下
public abstract class AbstractExecutorService implements ExecutorService {
    //此方法很简单就是对runnable保证,将其包装为一个FutureTask,FutureTask的解读在前文也讲解过
    protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
        return new FutureTask<T>(runnable, value);
    }
    //包装callable为FutureTask
    //之前说过FutureTask其实就是对Callable的一个封装
    protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
        return new FutureTask<T>(callable);
    }
    //提交一个Runnable类型的任务
    public Future<?> submit(Runnable task) {
        //如果为null则抛出NPE
        if (task == null) throw new NullPointerException();
        //包装任务为一个Future
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        //将任务丢给执行器,而此处会抛出拒绝异常,在讲述ThreadPoolExecutor的时候有讲述,不记得的读者可以去再看看
        execute(ftask);
        return ftask;
    }

    //与上方方法相同只不过指定了返回结果
    public <T> Future<T> submit(Runnable task, T result) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task, result);
        execute(ftask);
        return ftask;
    }
    //与上方方法相同只是换成了callable
    public <T> Future<T> submit(Callable<T> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task);
        execute(ftask);
        return ftask;
    }

    //执行集合tasks结果是最后一个执行结束的任务结果
    //可以设置超时 timed为true并且nanos是未来的一个时间
    //任何一个任务完成都将会返回结果
    private <T> T doInvokeAny(Collection<? extends Callable<T>> tasks,
                              boolean timed, long nanos)
        throws InterruptedException, ExecutionException, TimeoutException {
        //传入的任务集合不能为null
        if (tasks == null)
            throw new NullPointerException();
        //传入的任务数不能是0
        int ntasks = tasks.size();
        if (ntasks == 0)
            throw new IllegalArgumentException();
        //满足上面的校验后将任务分装到一个ArrayList中
        ArrayList<Future<T>> futures = new ArrayList<Future<T>>(ntasks);
        //并且创建一个执行器传入this
        //这里简单讲述他的执行原理,传入this会使用传入的this(类型为Executor)作为执行器用于执行任务,当submit提交任务的时候回将任务
        //封装为一个内部的Future并且重写他的done而此方法就是在future完成的时候调用的,而他的写法则是将当前完成的future添加到esc
        //维护的结果队列中
        ExecutorCompletionService<T> ecs =
            new ExecutorCompletionService<T>(this);

        try {
            //创建一个执行异常,以便后面抛出
            ExecutionException ee = null;
            //如果开启了超时则计算死线时间如果时间是0则代表没有开启执行超时
            final long deadline = timed ? System.nanoTime() + nanos : 0L;
            //获取任务的迭代器
            Iterator<? extends Callable<T>> it = tasks.iterator();
            //先获取迭代器中的第一个任务提交给前面创建的ecs执行器
            futures.add(ecs.submit(it.next()));
            //前面记录的任务数减一
            --ntasks;
            //当前激活数为1
            int active = 1;
            //进入死循环
            for (;;) {
                //获取刚才提价的任务是否完成如果完成则f不是null否则为null
                Future<T> f = ecs.poll();
                //如果为null则代表任务还在继续
                if (f == null) {
                    //如果当前任务大于0 说明除了刚才的任务还有别的任务存在
                    if (ntasks > 0) {
                        //则任务数减一
                        --ntasks;
                        //并且再次提交新的任务
                        futures.add(ecs.submit(it.next()));
                        //当前的存活的执行任务加一
                        ++active;
                    }
                    //如果当前存活任务数是0则代表没有任务在执行了从而跳出循环
                    else if (active == 0)
                        break;
                    //如果当前任务执行设置了超时时间
                    else if (timed) {
                        //则设置指定的超时时间获取
                        f = ecs.poll(nanos, TimeUnit.NANOSECONDS);
                        //等待执行超时还没有获取到则抛出超时异常
                        if (f == null)
                            throw new TimeoutException();
                        //否则使用当前时间计算剩下的超时时间用于下一个循环使用
                        nanos = deadline - System.nanoTime();
                    }
                    //如果没有设置超时则直接获取任务
                    else
                        f = ecs.take();
                }
                //如果获取到了任务结果f!=null
                if (f != null) {
                    //激活数减一
                    --active;
                    try {
                        //返回获取到的结果
                        return f.get();
                        //如果获取结果出错则包装异常
                    } catch (ExecutionException eex) {
                        ee = eex;
                    } catch (RuntimeException rex) {
                        ee = new ExecutionException(rex);
                    }
                }
            }
            //如果异常不是null则抛出如果是则创建一个
            if (ee == null)
                ee = new ExecutionException();
            throw ee;

        } finally {
            //其他任务则设置取消
            for (int i = 0, size = futures.size(); i < size; i++)
                futures.get(i).cancel(true);
        }
    }
    //对上方方法的封装
    public <T> T invokeAny(Collection<? extends Callable<T>> tasks)
        throws InterruptedException, ExecutionException {
        try {
            return doInvokeAny(tasks, false, 0);
        } catch (TimeoutException cannotHappen) {
            assert false;
            return null;
        }
    }
    //对上方法的封装
    public <T> T invokeAny(Collection<? extends Callable<T>> tasks,
                           long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException {
        return doInvokeAny(tasks, true, unit.toNanos(timeout));
    }
    //相对于上一个方法执行成功任何一个则返回结果而此方法是全部执行完然后统一返回结果
    public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
        throws InterruptedException {
        //传入的任务集合不能是null
        if (tasks == null)
            throw new NullPointerException();
        //创建一个集合用来保存获取到的执行future
        ArrayList<Future<T>> futures = new ArrayList<Future<T>>(tasks.size());
        //任务是否执行完成
        boolean done = false;
        try {
            //遍历传入的任务并且调用执行方法将创建的future添加到集合中
            for (Callable<T> t : tasks) {
                RunnableFuture<T> f = newTaskFor(t);
                futures.add(f);
                execute(f);
            }
            //遍历获取到的future
            for (int i = 0, size = futures.size(); i < size; i++) {
                Future<T> f = futures.get(i);
                //如果当前任务没有成功则进行f.get方法等待此方法执行成功,如果方法执行异常或者被取消将忽略异常
                if (!f.isDone()) {
                    try {
                        f.get();
                    } catch (CancellationException ignore) {
                    } catch (ExecutionException ignore) {
                    }
                }
            }
            //到这一步则代表所有的任务都已经有了确切的结果
            done = true;
            //返回任务结果集合
            return futures;
        } finally {
            //如果不是true是false 则代表执行过程中被中断了则需要对任务进行取消操作,如果正常完成则不会被取消
            if (!done)
                for (int i = 0, size = futures.size(); i < size; i++)
                    futures.get(i).cancel(true);
        }
    }
    //与上方方法的区别在于对于任务集合可以设置超时时间
    //这里会针对差异进行讲解
    public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
                                         long timeout, TimeUnit unit)
        throws InterruptedException {
        if (tasks == null)
            throw new NullPointerException();
        //计算设置时长的纳秒时间
        long nanos = unit.toNanos(timeout);
        ArrayList<Future<T>> futures = new ArrayList<Future<T>>(tasks.size());
        boolean done = false;
        try {
            for (Callable<T> t : tasks)
                futures.add(newTaskFor(t));
            //计算最终计算的确切时间点,运行时长不能超过此时间也就是时间死线
            //这里是个细节future创建的时间并没有算作执行时间
            final long deadline = System.nanoTime() + nanos;
            //获取当前结果数
            final int size = futures.size();
            //遍历将任务进行执行
            for (int i = 0; i < size; i++) {
                execute((Runnable)futures.get(i));
                //并且每次都计算死线
                nanos = deadline - System.nanoTime();
                //如果时间已经超过则返回结果
                if (nanos <= 0L)
                    return futures;
            }
            //否则遍历future确定每次执行都获取到了结果
            for (int i = 0; i < size; i++) {
                Future<T> f = futures.get(i);
                if (!f.isDone()) {
                    //如果在等待过程中已经超时则返回当前等待结合
                    if (nanos <= 0L)
                        return futures;
                    try {
                        //如果没有超过死线则设置从future中获取结果的时间如果超过则会派出timeout
                        f.get(nanos, TimeUnit.NANOSECONDS);
                    } catch (CancellationException ignore) {
                    } catch (ExecutionException ignore) {
                    } catch (TimeoutException toe) {
                        //抛出了异常则会返回当前的列表
                        return futures;
                    }
                    //计算最新的超时时间
                    nanos = deadline - System.nanoTime();
                }
            }
            //之前的返回都没有设置为true所以在finally中都会设置为取消唯独正常执行完成到此处返回的结果才是最终的结果
            done = true;
            return futures;
        } finally {
            if (!done)
                for (int i = 0, size = futures.size(); i < size; i++)
                    futures.get(i).cancel(true);
        }
    }

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值