Java线程池的基本工作原理及案例

一、线程池的优点

线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。

主要特点:线程复用;控制最大并发数;管理线程。

第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

第二:提高响应速度。当任务到达时,任务可以 不需要的等到线程创建就能立即执行。

第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

二、线程池3个常用方式

Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类。

1、Executors.newFixedThreadPool(5); // 一池5个处理线程

底层具体方法:

主要特点:

  • 1、创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

  • 2、newFixedThreadPool创建的线程池corePoolSize和maximumPoolSize值是相等的,它使用的LinkedBlockingQueue

适用:执行长期的任务,性能好很多

2、Executors.newSingleThreadExecutor(); // 一池1个处理线程

底层具体方法:

主要特点:

  • 1、创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。

  • 2、newSingleThreadExecutor将corePoolSize和maximumPoolSize都设置为1,它使用的LinkedBlockingQueue

适用:一个任务一个任务执行的场景

3、Executors.newCachedThreadPool(); // 一池N个处理线程

底层具体方法:

主要特点:

  • 1、创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

  • 2、newCachedThreadPool将corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,它使用的SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60s就销毁线程。

适用:执行很多短期异步的小程序或者负载较轻的服务器

4、代码案例

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class MyThreadPoolDemo {
    public static void main(String[] args) {
        // ExecutorService threadPool = Executors.newFixedThreadPool(5); // 一池5个处理线程
        // ExecutorService threadPool = Executors.newSingleThreadExecutor(); // 一池1个处理线程
        ExecutorService threadPool = Executors.newCachedThreadPool(); // 一池N个处理线程

        try {
            for (int i = 1; i <= 10; i++) {
                int temp = i;
                threadPool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "\t 办理业务" + temp);
                });
                // newCachedThreadPool案例:线程暂停一会
                try {
                    TimeUnit.MILLISECONDS.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }

    }
}

5、面试问题

你在工作中单一/固定数/可变三种创建线程池的方法,你使用的哪个多?(大坑)

答案:一个都不用,我们生产只能使用自定义的。

Executors中JDK已经给你提供了,为什么不用?

  • LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为Integer.MAX_VALUE即2147483647)阻塞队列。

三、线程池7个参数的含义

1、corePoolSize:线程池中的常驻核心线程数

线程池中的常驻核心线程数,在创建了线程池后,当有请求任务来之后,就会安排池中的线程去执行请求任务,近似理解为今日当值线程。

当线程中的线程数达到corePoolSize后,就会把到达的任务放到缓存队列当中。核心线程在allowCoreThreadTimeout 被设置为true时会超时退出,默认情况下不会退出。

2、maximumPoolSize:线程池能够容纳同时执行的最大线程

此值必须大于等于1。maximumPoolSize = 核心+非核心

3、keepAliveTime:多余的空闲线程的存活时间

当前线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime值时,多余空闲线程会被销毁直到只剩下corePoolSize个线程为上。

4、unit:keepAliveTime的单位

5、workQueue:任务队列,被提交但尚未被执行的任务

6、threadFactory:表示生成线程池中工作线程的线程工厂

用于创建线程一般用默认的即可

7、handler:拒绝策略

表示当队列满了并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时如何拒绝。

JDK内置的四种拒绝策略

  1. AbortPolicy (默认):直接抛出RejectedExecutionException异常阻止系统正常运行。

  1. CallerRunsPolicy:既不抛弃任务也不抛出异常,直接运行任务的run方法,换言之将任务回退给调用者来直接运行。使用该策略时线程池饱和后将由调用线程池的主线程自己来执行任务,因此在执行任务的这段时间里主线程无法再提交新任务,从而使线程池中工作线程有时间将正在处理的任务处理完成。

  1. DiscardOldestPolicy :抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务。

  1. DiscardPolicy : 直接丢弃任务任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种方案。

以上内置拒绝策略均实现了 RejectedExecutionHandler 接口,若以上策略仍无法满足实际需要,完全可以自己扩展 RejectedExecutionHandler 接口

其中,最重要的就是 核心线程数与最大线程数,因为其直接影响着 服务器的性能以及程序的响应速度

代码案例:

拒绝策略AbortPolicy,最大线程数=max+阻塞队列数即8,超过8会报异常

import java.util.concurrent.*;

public class T1 {
    public static void main(String[] args) {
        ExecutorService threadPool = new ThreadPoolExecutor(
                2,  // 顾客1,2
                5, // 顾客6,7,8
                100L,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(3),  // 等候区 顾客3,4,5
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

        try {
            for (int i = 1; i <= 8; i++) {  // 模拟8个顾客来办理业务,受理窗口max只有5个
                int temp = i;
                threadPool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "号窗口," + "\t 服务顾客" + temp);
                    try {
                        TimeUnit.SECONDS.sleep(4);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }
    }
}

输出信息:

四、线程池的底层工作原理

文字说明:

  1. 在创建了线程池后,等待提交过来的任务请求

  1. 当调用execute()方法添加一个请求时,线程池会做如下判断:

  1. 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务

  1. 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列

  1. 如果这个时侯队列满了且正在运行的线程数量还小于maxmumPoolSize,那么还是要创建非核心线程立刻运行这个任务

  1. 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行;

  1. 当一个线程完成任务时,它会从队列中取下一个任务来执行;

  1. 当一个线程无事可做超过一定时间(keepAliveTime)时,线程池会判断:

  1. 如果当前运行的线程数量大于corePoolSize,那么会将这个线程停掉。

  1. 所以线程池所有任务完成后它会最终收缩到corePoolSize的大小。

五、线程池配置核心线程数

很多时候,我们为了提高程序响应速度,会将耗时较长的代码交给线程池去异步执行,Java通过Executors提供四种线程池,分别newSingleThreadExecutor、newFixedThreadPoolnewScheduled、ThreadPool和newCachedThreadPool,但这四种并不推荐使用,因此需要自去自定义线程池

线程数的设置的重要目的是为了充分并合理地使用 CPU 和内存等资源,从而最大限度地提高程序的性能。

1、获取CPU核心数

CPU核心数

使用Runtime.getRuntime().availableProcessor()方法来获取。

2、判断线程处理的任务是哪种类型(CPU密集型和IO密集型

平时开发基本上都是IO密集型任务

CPU密集型

大部分时间用来做计算逻辑判断等CPU动作的程序称为CPU密集型任务。该类型的任务需要进行大量的计算,主要消耗CPU 资源, 这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多。 CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等千CPU的核心数。

但在通常情况下,会将核心线程数设置为CPU数+1,这个额外的线程可以确保 CPU 的时钟周期不会因为某线程阻塞而浪费;

CPU密集型:核心线程数 = CPU核心数 + 1

IO密集型

IO密集型任务指任务需要执行大量的IO操作,涉及到网络,磁盘IO操作,对CPU消耗较少,其消耗的主要资源为IO。IO操作的特点就是需要等待我们请求一些数据,由对方将数据与入缓冲区,在这段时间中,需要读取数据的线程根本无事可做,因此可以把CPU时间片让出去,直到缓冲区写满。

既然这样,IO密集型任务其实就有很大的优化空间了(毕竟存在等待)的时候。

由于IO密集型任务并不是一直在执行任务,则尽可能配置多的线程:
核心线程数 = CPU核心数 * 2

注:IO密集型(某大厂实践经验)

核心线程数 = CPU核数 / (1-阻塞系数) //阻塞系数在0.8~0.9之间
阻塞系数 = 阻塞时间 / (阻塞时间 + 计算时间)
比如:4/(1-0.8)=20 例如阻塞系数 0.8,CPU核数为4 则核心线程数为20

六、线程池的五种运行状态

1、running:线程池可以接收新的任务提交,并且还可以正常处理阻塞队列中的任务;

2、shoutdown:不再接收新的任务提交,不过线程池可以继续处理阻塞队列中的任务;

3、stop:不再接收新的任务,同时还会丢弃阻塞队列中的既有任务;此外,它还会中断正在处理的任务;

4、tidying:所有任务都执行完毕后(同时也涵盖了阻塞队列中的任务),当前线程池中的活动的线程数量降为0,将会调为terminated()方法进入terminated状态;

5、terminated:线程池的终止状态,当terminated方法执行完毕后,线程池将会处于该状态之下。

七、案例

线程池配置代码:

/**
 * 线程池
 */@Configuration@EnableAsyncpublicclassExecutorConfig {

    privatefinalstaticLoggerlog= LoggerFactory.getLogger(ExecutorConfig.class);

    @Resource
    Conf conf;

    @Beanpublic ThreadPoolTaskExecutor applicationTaskExecutor() {
        ThreadPoolTaskExecutortaskExecutor=newThreadPoolTaskExecutor();
        // 设置核心线程数
        taskExecutor.setCorePoolSize(conf.applicationTaskExecutorCorePoolSize);
        // 设置最大线程数
        taskExecutor.setMaxPoolSize(conf.applicationTaskExecutorMaxPoolSize);
        // 设置队列容量
        taskExecutor.setQueueCapacity(conf.applicationTaskExecutorQueueCapacity);
        // 设置线程活跃时间(秒)
        taskExecutor.setKeepAliveSeconds(conf.applicationTaskKeepAliveSeconds);
        // 设置默认线程名称
        taskExecutor.setThreadNamePrefix(conf.applicationTaskThreadNamePrefix);
        //调度器shutdown被调用时等待当前被调度的任务完成
        taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
        // 设置拒绝策略
        taskExecutor.setRejectedExecutionHandler(newThreadPoolExecutor.AbortPolicy());
        taskExecutor.initialize();
        return taskExecutor;
    }

    @Beanpublic ThreadPoolTaskExecutor taskAsyncPoolTaskExecutor() {
        ThreadPoolTaskExecutortaskExecutor=newThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(conf.camasTaskExecutorCorePoolSize);
        taskExecutor.setMaxPoolSize(conf.camasTaskExecutorMaxPoolSize);
        taskExecutor.setQueueCapacity(conf.camasTaskExecutorQueueCapacity);
        taskExecutor.setKeepAliveSeconds(conf.camasTaskKeepAliveSeconds);
        taskExecutor.setThreadNamePrefix(conf.camasTaskThreadNamePrefix);
        //调度器shutdown被调用时等待当前被调度的任务完成
        taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
        //等待时长//taskExecutor.setAwaitTerminationSeconds(conf.taskAwaitTerminationSeconds);
        taskExecutor.setRejectedExecutionHandler(newThreadPoolExecutor.AbortPolicy());
        taskExecutor.initialize();
        return taskExecutor;
    }
}
# 配置信息
conf:
  #探测主任务线程池
  #设置核心线程数
  applicationTaskExecutorCorePoolSize: 10
  #设置最大线程数
  applicationTaskExecutorMaxPoolSize: 10
  #设置队列容量
  applicationTaskExecutorQueueCapacity: 100
  #设置允许的空闲时间(秒)
  applicationTaskKeepAliveSeconds: 60
  applicationTaskAwaitTerminationSeconds: 60
  applicationTaskThreadNamePrefix: applicationTaskThreadPool-----

  #设置核心线程数
  camasTaskExecutorCorePoolSize: 50
  #设置最大线程数
  camasTaskExecutorMaxPoolSize: 50
  #设置队列容量
  camasTaskExecutorQueueCapacity: 65536
  #设置允许的空闲时间(秒)
  camasTaskKeepAliveSeconds: 60
  camasTaskAwaitTerminationSeconds: 60
  camasTaskThreadNamePrefix: camasTaskThreadPool-----

  #camas任务执行map中执行的任务数量
  taskRunningMapSize: 50
线程池使用代码:
@Component@Slf4jpublicclassCamasTaskCtl {

    // 任务执行map的总开关 true暂停 false运行publicstaticbooleanPause=true;

    publicbooleanisPause() {
        return Pause;
    }

    publicvoidsetPause(boolean pause) {
        Pause = pause;
    }

    /**
     * 当前执行任务的id,0 没有任务执行 >0 有正在执行的任务
     * 此值控制当前执行且仅执行1个任务
     * 当任务开始执行时,设置值,任务所有子任务执行完,将值设置为
     */publicstaticlongRunningTaskId=0;

    publicstatic Long getRunningTaskId() {
        return RunningTaskId;
    }

    publicstaticvoidsetRunningTaskId(Long runningTaskId) {
        CamasTaskCtl.RunningTaskId = runningTaskId;
    }

    @Autowired@Qualifier("taskAsyncPoolTaskExecutor")
    ThreadPoolTaskExecutor taskExecutor;

    publicstatic ConcurrentHashMap<String, TaskFather> RunningMap = newConcurrentHashMap<String, TaskFather>();

    @Resource
    Conf conf;
    @Resource
    TaskConf taskConf;
    @Resource
    CamasSurveyTaskMapper taskMapper;
    @Resource
    CamasSurveyTaskScanIpMapper taskScanIpMapper;
    @Resource
    CamasSurveyTaskScanLeakPortMapper taskScanLeakPortMapper;
    @Resource
    CamasSurveyTaskService taskService;
    @Resource
    AdminUserApi userApi;

    publicvoidwhileZctcTask() {
        // 项目启动时,应该只存在<1条的 status=1(正在执行)任务// 将status=1(正在执行) 改为status=2,人工在页面启动
        log.info("资产探测线程池控制方法,开始执行!");
        while (true) {
            try {
                Thread.sleep(1000 * 8);

                // 暂停开关开启,所有任务暂停if (Pause) {
                    continue;
                } elseif (RunningTaskId > 0 && RunningMap.size() >= conf.taskRunningMapSize) {
//                    log.info("任务执行map中任务数量:" + RunningMap.size() + " 不再添加任务! 当前执行任务RunningTaskId:" + RunningTaskId + " RunningMap:" + RunningMap);continue;
                }

                // 循环需执行的任务列表// 获取公司及部门下的所有用户
                Map<String, Object> params = newHashMap<>();
                List<AdminUserRespDTO> users = userApi.getByDeptId(taskConf.deptId);
                List<Long> ids = users.stream().map(AdminUserRespDTO::getId).collect(Collectors.toList());
                params.put("userArray", ids);
                // 查询需执行的任务列表
                List<Map> executingTaskList = taskMapper.selectExecutingTaskList(params);
                if (executingTaskList != null && executingTaskList.size() > 0) {
                    for (Map<String, Object> taskMap : executingTaskList) {
//                        log.info("获取所有未执行任务taskMap:" + taskMap.toString());intflag= Integer.valueOf(taskMap.get("flag").toString());
                        Longid= Long.valueOf(taskMap.get("id").toString());
                        // flag为1表示任务if (flag == 1) {
                            try {
                                CamasSurveyTaskDOtaskDO= taskMapper.selectById(id);
//                                log.info("需执行的任务:" + taskDO + "RunningTaskId:" + RunningTaskId);// RunningTaskId > 0表示当前有任务在执行if (RunningTaskId == 0 || RunningTaskId == taskDO.getId()) {
//                                    log.info("正在执行的任务:" + taskDO + "RunningTaskId:" + RunningTaskId);// 将任务的状态改为 1 正在执行
                                    taskDO.setStatus(1);
                                    if (RunningTaskId == 0) {
                                        taskDO.setPauseStartTime(newDate());
                                        taskDO.setPauseEndTime(newDate());
                                    }
                                    taskMapper.updateById(taskDO);

                                    // 设置RunningTaskId为当前正在执行的任务id
                                    RunningTaskId = taskDO.getId();

                                    // 查询当前任务需探测的Ip
                                    List<CamasSurveyTaskScanIpDO> taskScanIpDOList = taskScanIpMapper.selectTaskScanIpByTaskIdAndEndFlag(id, 0);
                                    List<CamasSurveyTaskScanIpDO> taskScanIpDOList3 = taskScanIpMapper.selectTaskScanIpByTaskIdAndEndFlag(id, 3);
                                    // 获取当前任务所有未探测的port
                                    List<CamasSurveyTaskScanLeakPortDO> taskScanLeakPortDOList = taskScanLeakPortMapper.selectTaskScanLeakPortByTaskIdAndEndFlag(id, 0);
                                    if ((taskScanIpDOList != null && taskScanIpDOList.size() > 0) || (taskScanIpDOList3 != null && taskScanIpDOList3.size() > 0)) {
                                        if ((taskDO.getProgress() == 100 || taskDO.getEndFlag() == 1)) {
                                            log.info("清空执行Map:任务进度100,结束标识=1");
                                            // 4任务失败
                                            taskDO.setStatus(4);
                                            taskDO.setRemark("清空执行Map:任务进度100,结束标识=1");
                                            taskMapper.updateById(taskDO);
                                            ClearRunningMap();
                                            RunningTaskId = 0;
                                            continue;
                                        }
                                        if (taskScanIpDOList != null && taskScanIpDOList.size() > 0) {
                                            for (CamasSurveyTaskScanIpDO taskScanIpDO : taskScanIpDOList) {
                                                // 如果任务执行map中没有满,则向map中添加任务执行对象 zcrwif (RunningMap.size() < conf.taskRunningMapSize) {
                                                    // 3正在执行
                                                    taskScanIpDO.setEndFlag(3);
                                                    taskScanIpMapper.updateById(taskScanIpDO);

                                                    TaskFathertaskFather=newSurveyTask(taskDO.getId(), taskScanIpDO.getIp(), taskScanIpDO.getId(), taskService, taskMapper, taskConf);
                                                    //任务执行map中,key定义为: 任务id--任务探测ip
                                                    log.info("任务执行map中--ip:" + taskScanIpDO.getIp() + "新增任务");
                                                    //1代表任务的类型:资产探测任务//                                                    log.info("1代表任务的类型:资产探测任务,CamasTaskCtl.RunningMap.size()" + CamasTaskCtl.RunningMap.size() + " key:" + 1 + "-" + taskDO.getId() + "-" + taskScanIpDO.getId());
                                                    RunningMap.put(1 + "-" + taskDO.getId() + "-" + taskScanIpDO.getId(), taskFather);
                                                    try {
                                                        // 通过线程池方式执行
                                                        taskExecutor.execute(taskFather);
                                                    } catch (Exception e) {
                                                        log.error("执行异常taskExecutor.execute" + e);
                                                        // 任务状态改为失败,e.getMessage()保存到备注字段// 2代表失败
                                                        taskDO.setEndFlag(2);
                                                        // 任务状态 0未执行1正在执行2等待执行3任务完成4任务失败
                                                        taskDO.setStatus(4);
                                                        // 当前任务结束时间
                                                        taskDO.setEndTime(newDate());
                                                        // e.getMessage()保存到备注字段
                                                        taskDO.setRemark(e.getMessage());
                                                        taskMapper.updateById(taskDO);
                                                        RunningTaskId = 0;
                                                        break;
                                                    }
                                                } else {
                                                    break;
                                                }
                                            }
                                        }
                                    } elseif (taskDO.getProgress() == 100 && taskDO.getEndFlag() == 1) {
                                        log.info("任务执行完毕,修改正在做执行任务id,RunningTaskId值为0,当前值:" + RunningTaskId + "taskDO.getProgress():" + taskDO.getProgress() + "taskDO.getEndFlag():" + taskDO.getEndFlag());
                                        RunningTaskId = 0;
                                    } else {
                                        // 将当前任务的结束标识修改为完成并修改任务结束时间// 1代表完成
                                        taskDO.setEndFlag(1);
                                        taskDO.setProgress(100.00);
                                        taskDO.setStatus(3);
                                        // 当前任务结束时间
                                        taskDO.setEndTime(newDate());
                                        longdiff= DateUtils.getNowDate().getTime() - taskDO.getPauseStartTime().getTime();
                                        if (Strings.isNotBlank(taskDO.getUsedTime())) {
                                            diff = diff + Long.valueOf(taskDO.getUsedTime());
                                        }
                                        taskDO.setUsedTime(String.valueOf(diff));
                                        taskMapper.updateById(taskDO);
                                        log.info("1任务执行完毕,修改正在做执行任务id RunningTaskId值为0,当前值:" + RunningTaskId + "taskDO.getProgress():" + taskDO.getProgress() + "taskDO.getEndFlag():" + taskDO.getEndFlag());
                                        RunningTaskId = 0;
                                    }
                                }
                            } catch (Exception ex) {
                                log.error("任务转换格式异常", ex);
                                continue;
                            }
                        }
                    }
                }

            } catch (Exception e) {
                log.error("任务队列执行异常!结束所有任务", e.getMessage());
                log.error(e.getMessage(), e);
                //删除所有进程break;
            }
        }
    }

    publicvoidClearRunningMap()throws Exception {
        log.info("暂停--所有任务,所有执行中的任务暂停!");
        RunningTaskId = 0;
        //然后就看任务执行的地方for (Map.Entry<String, TaskFather> m : CamasTaskCtl.RunningMap.entrySet()) {
            TaskFathertaskFather= m.getValue();
            taskFather.setPause(true);
        }
        log.info("暂停--所有任务,清空任务执行map,CamasTaskCtl.RunningMap:" + CamasTaskCtl.RunningMap.size());
        CamasTaskCtl.RunningMap.clear();
        //执行kill命令
        log.info(taskConf.killlc2022);
        InputStreamfis=null;
        InputStreamReaderisr=null;
        BufferedReaderbr=null;
        Processprocess= Runtime.getRuntime().exec(taskConf.killlc2022);
        //取得命令结果的输入流//fis = process.getErrorStream();
        fis = process.getInputStream();
        //用一个读输入流类去读
        isr = newInputStreamReader(fis);
        //用缓冲器读行
        br = newBufferedReader(isr);
        Stringline=null;
        doublebfb=0;

        Integeripid=null;

        //直到读完为止while ((line = br.readLine()) != null) {
            log.info(line);
        }
    }
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值