线程池

概述

线程是一种稀缺的资源,每个线程的创建需要分配部分内存作为栈空间,所以如果每个任务都创建一个新的线程容易导致内存溢出,同时CPU的时间片有限,大量线程涌出会导致频繁的线程上下文切换问题,于是产生了线程池

合理使用线程池能够带来三个好处:

  1. 降低资源消耗,通过重复利用已有的线程,降低线程创建和销毁造成的资源消耗
  2. 提高响应速度,当任务到达时,可以直接在已有的线程上执行,不需要等待线程创建的时间
  3. 提高线程的可管理性,线程时稀缺资源,无限制的创建不光会消耗系统资源,而且会影响系统的稳定性,使用线程池可以对所有线程进行统一分配、调优和监控

线程池的组成

  • 核心线程:包含指定数量的核心线程数,需要执行任务时,由这些线程来处理
  • 阻塞队列:当线程池中的线程达到最大线程数,任务会进入阻塞队列排队,等待空闲线程的出现
  • 拒绝策略:当线程池满了,同时阻塞队列也满了,再来新的任务就需要进行拒绝,怎么拒绝任务、拒绝哪些任务就是拒绝策略需要规定的

自定义线程池

/***
 * @author shaofan
 * @Description 自定义线程池
 */
@Slf4j
public class ThreadPoolTest{
    public static void main(String[] args) throws InterruptedException {
        // 如果队列已满,则死等队列有空闲
        RejectPolicy<Runnable> alwaysWait = (queue,task)->{queue.put(task);};
        // 如果队列已满,则等待1s
        RejectPolicy<Runnable> waitSec = (queue,task)->{queue.offer(task,1,TimeUnit.SECONDS);};
        // 如果队列已满,则放弃入队
        RejectPolicy<Runnable> giveUp = (queue,task)->{};
        // 如果队列已满,则抛出异常
        RejectPolicy<Runnable> error = (queue,task)->{throw new RuntimeException("队列已满");};
        // 如果队列已满,则由调用线程自己执行
        RejectPolicy<Runnable> runByMySelf = (queue,task)->{task.run();};

        MyThreadPool threadPool = new MyThreadPool(2,1000,TimeUnit.MILLISECONDS,10,error);
        for (int i = 0; i < 20; i++) {
            int j = i;
            threadPool.execute(()->{
                try {
                    Thread.sleep(100000000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("{}",j);
            });
        }
    }
}

/***
 * 拒绝策略接口,策略模式,定义队列满了之后的操作,与入队操作解耦
 */
@FunctionalInterface
interface RejectPolicy<T>{
    /**
     * 当队列满了的拒绝操作
     * @param queue
     * @param task
     */
    void reject(BlockingQueue<T> queue,T task) throws InterruptedException;
}

/***
 * 自定义线程池
 */
@Slf4j
class MyThreadPool {

    /**
     * 任务队列
     */
    private BlockingQueue<Runnable> taskQueue;

    /**
     * 线程容器
     */
    private HashSet<Worker> workers = new HashSet<>();

    /**
     * 核心线程数
     */
    private int coreSize;

    /**
     * 任务超时时间
     */
    private long timeout;

    /**
     * 任务超时时间单位
     */
    private TimeUnit unit;

    /**
     * 拒绝策略
     */
    private RejectPolicy<Runnable> rejectPolicy;

    public MyThreadPool(int coreSize,int timeout,TimeUnit unit,int queueCapacity,RejectPolicy<Runnable> rejectPolicy){
        this.coreSize = coreSize;
        this.timeout = timeout;
        this.unit = unit;
        this.taskQueue = new BlockingQueue<>(queueCapacity);
        this.rejectPolicy = rejectPolicy;
    }

    /**
     * 执行任务
     * @param task
     */
    public void execute(Runnable task) throws InterruptedException {
        synchronized (workers){
            // 如果线程池中的线程数没有达到核心线程数,则创建新的线程
            if(workers.size()<coreSize){
                Worker worker = new Worker(task);
                log.debug("新增worker-{},{}",worker,task);
                workers.add(worker);
                worker.start();
            }else{
                // 如果线程数达到了核心线程数,则将任务存入任务队列中
                log.debug("任务入队-{}",task);
                taskQueue.tryPut(rejectPolicy,task);
            }
        }

    }

    /***
     * 线程池中的线程
     */
    class Worker extends Thread{
        private Runnable task;

        Worker(Runnable task){
            this.task = task;
        }

        @SneakyThrows
        @Override
        public void run() {
            // 当任务不为空,则执行任务;当任务为空,则从任务队列中取得新的任务
            while(task != null || (task = taskQueue.take()) != null){
                try{
                    log.debug("正在执行{}",task);
                    task.run();
                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    task = null;
                }
            }
            // 如果任务队列中也没有新的任务了,将当前线程从线程池中移除
            synchronized (workers){
                log.debug("worker 被移除{}",this);
                workers.remove(this);
            }
        }
    }
}

/***
 * 阻塞队列,任务会进入阻塞队列有线程读取
 * @param <T>
 */
@Slf4j
class BlockingQueue<T>{
    /**
     * 任务队列
     */
    private Deque<T> queue = new ArrayDeque<>();

    /**
     * 任务锁,多个线程从队列中读取任务时,保证线程安全
     */
    private ReentrantLock lock = new ReentrantLock();

    /**
     * 生产者条件变量,队列满时不能生产,生产线程等待
     */
    private Condition fullWaitSet = lock.newCondition();

    /**
     * 消费者条件变量,队列为空时不能消费,消费线程等待
     */
    private Condition emptyWaitSet = lock.newCondition();

    /**
     * 容量
     */
    private int capacity;

    public BlockingQueue(int capacity){
        this.capacity = capacity;
    }


    /***
     * 阻塞超时的获取任务
     * @param timeout
     * @param unit
     * @return
     * @throws InterruptedException
     */
    public T poll(long timeout, TimeUnit unit) throws InterruptedException {
        lock.lock();
        try{
            long nanos = unit.toNanos(timeout);
            while(queue.isEmpty()){
                // 以剩余的时间作为退出等待的条件
                if(nanos<=0){
                    return null;
                }
                // 获取剩余的时间
                nanos = emptyWaitSet.awaitNanos(nanos);
            }
            T t = queue.removeFirst();
            // 唤醒生产线程
            fullWaitSet.signalAll();
            return t;
        }finally {
            lock.unlock();
        }
    }

    /**
     * 从队列中阻塞获取
     * @return
     * @throws InterruptedException
     */
    public T take() throws InterruptedException {
        lock.lock();
        try{
            while(queue.isEmpty()){
                emptyWaitSet.await();
            }
            T t = queue.removeFirst();
            // 唤醒生产线程
            fullWaitSet.signalAll();
            return t;
        }finally {
            lock.unlock();
        }
    }

    /**
     * 阻塞添加任务
     * @param task
     * @throws InterruptedException
     */
    public void put(T task) throws InterruptedException {
        lock.lock();
        try{
            while(queue.size()==capacity){
                log.debug("等待加入任务队列");
                fullWaitSet.await();
            }
            queue.addLast(task);
            // 唤醒消费线程
            emptyWaitSet.signalAll();
        }finally {
            lock.unlock();
        }
    }

    /**
     * 带有超时时间的阻塞添加
     * @param task
     * @param timeout
     * @param unit
     * @return
     * @throws InterruptedException
     */
    public boolean offer(T task,long timeout,TimeUnit unit) throws InterruptedException {
        lock.lock();
        try{
            long nanos = unit.toNanos(timeout);
            while(queue.size()==capacity){
                if(nanos<=0){
                    return false;
                }
                log.debug("等待加入任务队列");
                nanos = fullWaitSet.awaitNanos(nanos);
            }
            queue.addLast(task);
            // 唤醒消费线程
            emptyWaitSet.signalAll();
            return true;
        }finally {
            lock.unlock();
        }
    }

    /**
     * 带有拒绝策略的入队
     * @param rejectPolicy
     * @param task
     */
    public void tryPut(RejectPolicy<T> rejectPolicy,T task){
        lock.lock();
        try{
            try{
                // 队列已满,执行拒绝策略定义的操作
                if(queue.size() == capacity){
                    rejectPolicy.reject(this,task);
                }else{
                    //队列还有空闲直接入队
                    log.debug("任务入队-{}",task);
                    queue.addLast(task);
                    emptyWaitSet.signalAll();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }finally {
            lock.unlock();
        }
    }

    public int size(){
        return queue.size();
    }
}

ThreadPoolExecutor

在这里插入图片描述

线程池状态

ThreadPoolExecutor使int的高三位来表示线程池状态,低27位表示线程数量

  • RUNNING:高三位为111,可以接收新任务和处理阻塞队列的任务,表示线程池正常工作中
  • SHUTDOWN:高三位为000,不能接收新任务,可以处理阻塞队列的任务,线程池调用shutdown方法时表示关闭线程池,但shutdown会等线程池中的任务执行完毕再关闭
  • STOP:高三位为001,不能接收新任务也不能处理阻塞队列的任务,线程池调用shutdownNow方法时,强制关闭线程,会中断正在执行当任务并抛弃任务队列的任务
  • TIDYING:高三位为010,任务全部执行完毕,活动线程为0,即将进入终结
  • TERMINATED:高三位为011,终结状态

从数字上比较,TERMINATED>TIDYING>SOP>SHUTDOWN>RUNNING

将线程状态和线程个数存储再一个原子变量ctl中,目的是将线程池状态与线程个数合二为一,这样就可以用一次cas操作进行赋值

线程池的创建

public ThreadPoolExecutor(int corePoolSize,
						  int maximumPoolSize,
						  long keepAliveTime,
						  TimeUnit unit,
						  BlockingQueue<Runnable> workQueue,
						  ThreadFactory threadFactory,
						  RejectedExecutionHandler handler)
  • corePoolSize:核心线程数,当线程池中的线程没有达到这个数量,每来一个任务就会创建一个线程,不论当前是否能够直接处理这个任务,当线程数达到这个数量才会优先使用已有的线程
  • maximumPoolSize:最大线程数,线程池允许创建的最大线程数量,如果队列满了并且已创建的线程数小于这个值,则线程池会再创建新的救急线程来执行这个任务;如果阻塞队列是无限的这个参数就没有效果
  • keepAliveTime:针对救急线程(核心线程以外的线程)的空闲存活时间,当救急线程空闲后,超过这个时间会被释放
  • unit:keepAliveTime的时间单位
  • workQueue:阻塞队列,用于保存等待执行任务的队列,当线程数达到了核心线程数时,新来的任务不会直接创建线程执行,而是进入阻塞队列
  • ThreadFactory:创建线程的线程工厂,可以通过线程工厂来给每个创建出来的线程创建更有意义的名字,代码:new ThreadFactoryBuilder().setNameFormat("XX-task-%d").build()(框架guava)
  • handler:拒绝策略,当阻塞队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略来处理新来的任务

可选的阻塞队列

  1. ArrayBlockingQueue:一个基于数组结构的有界阻塞队列,此队列按FIFO原则对数据进行排序
  2. LinkedBlockingQueue:一个基于连边结构的阻塞队列,此队列按FIFO排序元素,吞吐量要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列
  3. SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作必须要等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool()使用了这个队列
  4. PriorityBlockingQueue:一个具有优先级的无限阻塞队列

可选的拒绝策略

  1. AbortPolicy:直接抛出异常,默认策略
  2. CallerRunsPolicy:由调用方的线程来执行这个任务
  3. DiscardOldestPolicy:丢弃队列中最近的一个任务,并执行当前任务
  4. DiscardPolicy:丢弃任务
  5. Dubbo的实现:再抛出RejectedExecutionException异常之前会记录日志,并dump线程栈信息,方便定位问题
  6. Netty的实现:创建一个新的线程来执行任务
  7. ActiveMQ的实现:待超时等待(60s)尝试放入队列
  8. PinPoint的实现:使用了一个拒绝策略链,会注意尝试策略链中的每种拒绝策略

newFixedThreadPool(固定大小的线程池)

public static ExecutorService newFixedThreadPool(int nThreads){
	return new ThreadPoolExecutor(nThreads,nThreads,
									0L,TimeUnit.MILLISECONDS,
									new LinkedBlockingQueue<Runnable>());
}
  • 核心线程数=最大线程数,没有救急线程,因此也无需超时时间
  • 阻塞队列是无界的,可以方任意数量的任务
  • 适用于任务量一致,相对耗时的任务

newCachedThreadPool

public static ExecutorService newCachedThreadPool(){
	return new ThreadPoolExecutor(0,Integer.MAX_VALUE,
									60L,TimeUnit.SECONDS,
									new SynchronousQueue<Runnable>());
}
  • 核心线程数是0,最大线程数是Integer.MAX_VALUE,救急线程的空闲生存时间是60s,意味着全部都是救急线程,救急线程可以无限创建
  • 队列采用了SynchronousQueue实现,内部没有容量,每一个任务都会对应一个线程直接执行,没有空闲线程就创建救急线程

newSingleThreadExecutor

public static ExecutorService newSingleThreadExecutor(){
	return new FinalizableDelegatedExecutorService(
		new ThreadPoolExecutor(1,1,0L,
								TimeUnit.MILLISECONDS,
								new LinkedBlockingQueue<Runnable>()));
}

使用场景:希望多个任务排队执行,线程数固定为1,任务数多余1时,会放入无界队列排队,任务执行完毕,这唯一的线程也不会被释放

区别

  • 自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何不久措施,而线程池还会新建一个线程,保证线程池的正常工作
  • Executors.newSingleThreadExecutor()线程个数始终为1,不可更改;应用装饰器模式,对外暴露了ExecutorService接口,因此不能调用ThreadPoolExecutor中特有的方法
  • Executors.newFixedThread(1)初始为1,但还可以修改,对外暴露ThreadPoolExecutor对象,可以强转后调用setCorePoolSize等方法进行修改

任务的提交

线程池中任务的提交可以通过execute、submit、invokeAll、invokeAny四种方法来执行

execute

execute用于提交不需要返回值的任务,所以不知道任务是否执行完成、什么时候执行完成

threadPool.execute(new Runnable() {
            @Override
            public void run() {
                //任务代码
            }
        });

submit

submit用于提交有返回值的任务,得到一个Future类型的对象,可以通过future.get()来阻塞获取任务的返回值,或者通过future.get(long timeout,TimeUnit unit)来超时获取任务的返回值

Future<Boolean> future = threadPool.submit(new Callable<Boolean>() {
            @Override
            public Boolean call() throws Exception {
                return true;
            }
        });
        log.debug(String.valueOf(future.get()));

invokeAll

invokeAll可提交多个任务,有两种重载的形式,一种是阻塞执行,另一种是带有超时时间的,带有超时时间的形式如果任务执行超时会抛出异常

List<Future<Object>> futures = threadPool.invokeAll(Arrays.asList(
                ()->{
                    return true;
                },
                ()->{
                    Thread.sleep(2000);
                    return 1;
                },
                ()->{
                    return "ok";
                }
        ),1,TimeUnit.SECONDS);

        futures.forEach(f->{
            try {
                log.debug("{}",f.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        });

invokeAny

invokeAny中所有任务都会被提交,但是只有一个能够成功执行,哪个任务先执行完毕,返回此任务执行结果,其他任务取消,同样也有带超时时间的重载方法

Object o = threadPool.invokeAny(Arrays.asList(
                ()->{
                    return true;
                },
                ()->{
                    Thread.sleep(2000);
                    return 1;
                },
                ()->{
                    return "ok";
                }
        ));
        log.debug("{}",o);

关闭线程池

shutdown

平和的关闭线程池,线程池状态变为SHUTDOWN,此时不会接收新的任务,但是会将已经提交的任务执行完,不会阻塞调用线程的执行

public void shutdown(){
	final ReentranLock mainLock = this.mainLock;
	mainLock.lock();
	try{
		checkShutdownAccess();
		// 修改线程池状态
		advanceRunState(SHUTDOWN);
		// 打断空闲线程
		interruptIdleWorkers();
		onShutdown();// 子类ScheuledThreadPoolExecutor扩展点
	}finally{
		mainLock.unlock();
	}
	// 尝试进行终结,没有运行的线程可以立刻终结,如果还有
	tryTerminate();
}

shutdownNow

立即关闭线程池,线程池的状态将变为STOP,此时不会接收新的任务,并且将队列中的任务返回,并用interrupt方法中断正在执行的任务

public List<Runnable> shutdownNow(){
	List<Runnable> tasks;
	final ReentrantLock mainLock = this.mainLock;
	mainLock.lock();
	try{
		checkShutdownAccess();
		// 修改线程池的状态
		advanceRunState(STOP);
		// 打断所有线程
		interruptWorkers();
		// 获取队列中所有剩余任务
		tasks.dranQueue();
	}finally{
		mainLock.unlock();
	}
	// 尝试终结
	tryTerminate();
	return tasks;
}

任务调度线程池-ScheduledThreadPoolExecutor

任务调度线程池用于完成任务调度功能(延时或定时执行任务),在任务调度线程池之前的任务调度都是使用Timer类来实现,Timer类简单易用,但是有一个明显的缺点:所有任务都是都同一个线程来调度,所以所有任务都是串行执行的,同一时间只有一个任务在执行,前一个任务的延迟或异常都会影响到后一个任务

Timer

@Slf4j
public final class Demo{
    public static void main(String[] args){
        Timer timer = new Timer();
        TimerTask task1 = new TimerTask() {
            @Override
            public void run() {
                log.debug("first");
            }
        };
        TimerTask task2 = new TimerTask() {
            @Override
            public void run() {
                log.debug("second");
            }
        };

        timer.schedule(task1,1000L);
        timer.schedule(task2,2000L);
    }
}

ScheduledThreadPoolExecutor

延时执行

@Slf4j
public final class Demo{
    public static void main(String[] args){
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(10);
        pool.schedule(()->{
            log.debug("ok");
        },2000L,TimeUnit.MILLISECONDS);
    }
}

定时执行

@Slf4j
public final class Demo{
    public static void main(String[] args){
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(10);
        pool.scheduleAtFixedRate(()->{
            log.debug("ok");
        },0L,1000L,TimeUnit.MILLISECONDS);
    }
}

ScheduledThreadPoolExecutor应用-定时任务

在每周一零点执行操作,如数据备份等

@Slf4j
public class TestScheduled {
    public static void main(String[] args) {
        LocalDateTime now = LocalDateTime.now();
        // 构造执行任务的时间
        LocalDateTime time = now.withHour(0).withMinute(0).withSecond(0).withNano(0).with(DayOfWeek.MONDAY);

        // 如果本周四已经过去了,直接加一周到下周四一
        if(now.compareTo(time)>0){
            time = time.plusWeeks(1);
        }

        // 获取从现在到指定时间的时间间隔
        long initDelay = Duration.between(now,time).toMillis();
        // long initDelay = 0;
        // 定时时间间隔,使用1000测试,正式场景使用一周的毫秒数
        long period = 1000;
        ScheduledExecutorService pool = new ScheduledThreadPoolExecutor(1);
        pool.scheduleAtFixedRate(()->{
            log.debug("operation");
        },initDelay,period, TimeUnit.MILLISECONDS);

    }
}

tomcat线程池

在这里插入图片描述

  • LimitLatch用来限流,可以控制最大连接个数,类似JUC中的Semaphore
  • Acceptor只负责接收新的socket连接
  • Poller只负责监听socker channel是否有可读的I/O事件
  • 一旦有可读事件发生,封装一个任务对象(socketProcessor),提交给Executor线程池处理
  • Executor线程池中的工作线程最终负责处理请求

Tomcat线程池扩展了ThreadPoolExecutor,行为稍有不同:

  • 如果总线程数达到了maximumPoolSize,不会立刻抛出RejectedExecutionException,而是再次尝试将任务放入队列,如果还失败,才抛出异常

相关配置

Connector配置

配置项默认值说明
acceptorThreadCount1acceptor线程数量
pollerThreadCount1poller线程数量
minSpareThreads10核心线程数
maxThreads200最大线程数
executor-Executor名称,用来引用Executor

Executor线程配置

配置项默认值说明
threadPriority5线程优先级
daemontrue是否是守护线程
minSpareThreads25核心线程数
maxThreads200最大线程数
maxIdleTime60000线程生存时间
maxQueueSizeInteger.MAX_VALUE队列长度
prestartminSpareThreadsfalse核心线程是否在服务器启动时启动

执行流程

在这里插入图片描述

Fork/Join线程池

  • Fork/Join是JDK1.7加入的新的线程池实现,它体现的是一种分治思想,适用于能够进行任务拆分的cpu密集型计算
  • 所谓任务拆分,是根据分治思想,将一个大任务拆分为算法上相同的小任务,直至不能拆分可以直接求解,跟递归相关的一些计算
  • Fork/Join在分治基础上加入了多线程,可以把每个任务分解和合并交给不同的线程来完成,进一步提升了运算效率
  • Fork/Join默认创建与cpu核心数大小相同的线程池

工作窃取算法

工作窃取算法是指某个线程从其他队列里来窃取任务执行。在Fork/Join中,将一个大任务分割成若干个互不影响的子任务,为了减少线程之间的竞争,会把这些子任务分别放到不同队列里,每个队列对应一个线程来处理,当某个线程的队列中没有任务了,就可以去帮其他队列分担任务
优点
充分利用线程进行并行计算,减少了线程之间的竞争
缺点
在队列中只有一个任务时,会存在竞争,而且需要创建多个队列,消耗了更多的资源

Fork/Join的设计

  1. ForkJoinTask:Fork/Join线程池中的任务类,他提供在任务中执行fork()和join()操作的机制。通常情况下,我们不需要直接继承ForkJoinTask类,只需要继承他的子类,Fork/Join框架提供了以下两个子类:
    • RecursiveAction:用于没有返回结果的任务,类比Runnable
    • RecursiveTask:用于有返回结果的任务,类比Callable
  2. ForkJoinPool:ForkJoinTask需要通过ForkJoinPool来执行,任务分割出来的子任务会添加到当前工作线程所维护的双端队列中,当一个工作线程的队列里面没有任务是,他会随机从其他工作线程的队列的尾部获取一个任务(工作窃取算法)

使用Fork/Join

使用Fork/Join线程池,完成自然序列求和:

@Slf4j
public class TestForkJoin {
    public static void main(String[] args) {
        ForkJoinPool pool = new ForkJoinPool();

        MyTask task = new MyTask(0,100);
        Future<Integer> result = pool.submit(task);
        try{
            log.debug("{}", result.get());
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
class MyTask extends RecursiveTask<Integer>{
    private int start;
    private int end;
    private final int THRESHOLD = 10;

    public MyTask(int start,int end){
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        int sum = 0;
        // 如果间隔超过了阈值则分割处理,没有超过阈值则直接处理
        boolean canCompute = (start-end)<THRESHOLD;
        if(canCompute){
            for (int i = start; i < end; i++) {
                sum += i;
            }
        }else{
            // 取中点分割成两个子任务
            int mid = (start+end)>>1;
            MyTask leftTask = new MyTask(start,mid);
            MyTask rightTask = new MyTask(mid+1,end);

            // 两个子任务分别提交
            leftTask.fork();
            rightTask.fork();

            // 将两个子任务汇总
            sum += leftTask.join()+rightTask.join();
        }
        return sum;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值