JAVA 异步编程(异步,线程,线程池)一

目录

1.概念

1.1 线程和进程的区别

1.2 线程的五种状态

1.3 单线程,多线程,线程池

1.4 异步与多线程的概念

2. 实现异步的方式

2.1 方式1 裸线程(Thread)

2.1.1 继承Threade并重写run方法实现多线程

2.1.2 实现Runnable接口

2.1.3 实现Callable接口

2.1.4 匿名内部类(Callable(),Runnable(),Threadl类和lambda表达式)

2.1 方式2 线程池(Executor)

       2.1.1 源码分析

2.1.2  线程池创建(Executors)

 2.1.3 阻塞主线程获取子线程返回值

1.线程池awaitTermination轮询

2.线程池future.get

3. CountDownLatch类的await(推荐)

4.线程池invokeAll 

5. ExecutorCompletionService(强烈推荐)

2.1.4 非阻塞主线程获取子线程返回值

1. Future接口:

2. CompletableFuture类:

2.3 方式3 ForkJoinPool(并行计算java8+)

2.4 方式4 Spring的Async注解

4. 总结:


1.概念

1.1 线程和进程的区别

        进程:是一个动态的过程,是一个活动的实体,简单来说,一个应用程序的运行就可以看作是一个进程。可以说,进程中包含了多个可以同时运行的线程。

线程(Thread):是运行中实际任务的执行者,线程是操作系统能够进行运算调度的最小单位。它被包装在进程中,是进程中的实际运行单位。每个线程都有自己的程序计数器、堆栈和局部变量,但它们共享进程的代码和内存。

1.2 线程的五种状态

  1. 新建(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new Thread()

  2. 就绪(Runnable):也被称为“可执行”状态,当线程对象调用start()方法(启动线程)后,线程即进入就绪状态。处于就绪状态的线程,只是说明了该线程可以运行,但还没有真正运行,等待CPU分配时间片。

  3. 运行(Running):当CPU开始调度处于就绪状态的线程时,线程进入运行状态,真正开始执行线程代码。

  4. 阻塞(Blocked):线程在运行过程中可能因为各种原因导致无法继续执行,比如等待I/O操作结果,或者尝试获得一个同步监视器而失败,这时它就会进入阻塞状态。

  5. 死亡(Dead):线程执行完毕或者因异常退出run()方法后,线程就进入死亡状态。

1.3 单线程,多线程,线程池

单线程: 顾名思义是只有一条线程在执行任务,在工作中很难遇到。

多线程: 是创建多条线程同时执行任务。

线程池(ThreadPool): 是一种管理线程的机制,它能够复用线程,避免因频繁创建和销毁线程导致的性能问题。通过设置线程池的大小,可以有效管理线程的运行。

1.4 异步与多线程的概念

        异步和多线程并不是同一关系,异步是最终目的,多线程只是我们实现异步的一种手段。异步是调用者发送一个请求给被调用者,而调用者不用等待请求结果的返回,可以去做其他事。实现异步可以使用多线程或交给其他进程来处理。

2. 实现异步的方式

Util类:

public class Util {

    /**
     * sleep
     *
     * @param milliseconds
     */
    public static void mySleep(int milliseconds) {
        try {
            TimeUnit.MILLISECONDS.sleep(milliseconds);
        } catch (InterruptedException e) {

        }
    }

    /**
     * print log
     * @param message
     */
    public static void printfLog(String message) {
        LocalDateTime localDateTime = LocalDateTime.now();
        String dateString = localDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"));
        System.out.println(String.format("%s - %s", dateString, message));
    }
}

2.1 方式1 裸线程(Thread)

        使用“原汁原味”的裸线程(Thread)。Java线程本质上被映射到操作系统线程,并且每个线程对象对应着一个计算机底层线程。

JVM管理着线程的生存期,而且只要你不需要线程间通讯,你也不需要关注线程调度。

每个线程有自己的栈空间,它占用了JVM进程空间的指定一部分。

2.1.1 继承Threade并重写run方法实现多线程

2.1.2 实现Runnable接口

2.1.3 实现Callable接口

2.1.4 匿名内部类(Callable(),Runnable(),Threadl类和lambda表达式)

    private static void threadMethod(List<Integer> list) {
        while (list.get(0) > 0) {
            Util.mySleep(Double.valueOf(Math.random() * 100).intValue());
            Integer result = list.get(0);
            result--;
            list.add(0, result);
        }
    }
    public static void threadTest() {
        Util.printfLog("主线程开始");
        //线程外的变量只读,值类型只能显示的生命final
        //这里正常应该会输出list result=0,但是这里因为时多个线程同时操作一个变量导致线程不安全,输出list result为负值
        List<Integer> list = new ArrayList<>(1);
        list.add(100);
        for (int i = 0; i < 100; i++) {
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    threadMethod(list);
                    Util.printfLog("子线程结束!currentName = " + Thread.currentThread().getName());
                }
            };
            Thread thread = new Thread(runnable);
            thread.start();
        }
        Util.printfLog("主线程结束");
        Util.mySleep(10000);
        Util.printfLog("list result=" + list.get(0));
    }

这里暂时没考虑线程安全行,所以list result可能会出现负值。

2.1 方式2 线程池(Executor)

2.1.1 源码分析

 Executor一个接口,它通过一系列抽象类,接口等最终生成了线程池ThreadPoolExecutor。

它的包在java.util.concurrent。线程池的底层实现也是通过一系列的操作,通过Thread创建单独的线程。

ExecutorService.submit->AbstractExecutorService.submit->ThreadPoolExecutor.execute->
ThreadPoolExecutor.addWorker->Worker构造函数->DefaultThreadFactory.newThread

也可以使用工具类Executors创建线程池,底层也是通过创建ThreadPoolExecutor创建线程池。

ThreadPoolExecutor的主要方法:

方法名描述
submit

Callable<T>的实现类,创建带返回值的线程

Runnable的实现类,创建不带返回值的线程

shutdown优雅的终止线程。线程池中的线程不会立即结束,等线程池中没有在运行的线程才终止后台的线程池。
shutdownNow直接终止线程。不管线程池中有没有在运行的线程,直接将后台的线程池终止。

2.1.2  线程池创建(Executors)

线程池Executors工具类的主要方法:

方法名描述
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newSingleThreadExecutor创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
newScheduledThreadPool

创建一个定长线程池,支持定时及周期性任务执行。

ScheduledExecutorService的方法:

        schedule:定时到达时间间隔执行。
        scheduleAtFixedRate:周期,从上一个任务开始运行就开始计时
        scheduleWithFixedDelay:周期,从上一次任务运行结束开始计时

    public static void main(String[] args) throws IOException {
        System.out.println("主线程【开始】");
        executorsNoWait();
        System.out.println("主线程【结束】");
        System.in.read();
}    

/**
     * 线程直接提交,提交完成之后直接直接回到主进程
     */
    private static void executorsNoWait() {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        executorService.submit(() -> {
            threadMethod(5, "线程--1");
            return "第一线程";
        });
        executorService.submit(() -> {
            threadMethod(2, "线程--2");
            return "第二线程";
        });
        // 线程池没shutdwon,后台运行
        executorService.shutdown();
    }


private static void threadMethod(int num, String name) {
        for (int i = 0; i < num; i++) {
            Util.mySleep(1000);
            Util.printfLog(String.format("%s", name));
        }
    }

这种方式创建只提交线程到线程池,不阻塞主线程,不获取线程的返回值。通过代码运行结果,主线程已经执行完成,但是线程池中的子线程还没结束所以没有获取结果就不会阻塞主线程的运行。 只有当线程池中的没有runing的线程,线程池才会shutdown。

2.1.3 线程池运行线程判断

有时候我们需要等待线程池的所有任务都执行完,然后再进行下一步操作。对于线程 Thread 来说,很好实现,加一个 join 方法(主线程”等待“子线程”结束之后才能继续运行)就解决了

方式1:  isTerminated 方法
        threadPool.shutdown();
        while (!threadPool.isTerminated()) { // 如果没有执行完就一直循环
        }
方式2:getCompletedTaskCount

通过获取线程池计划执行任务数=已完成任务数

 while (threadPool.getTaskCount() != threadPool.getCompletedTaskCount()) {
    }
方式3:CountDownLatch(强烈推荐) 

CountDownLatch 可以理解为一个计数器,我们创建了一个包含 N 个任务的计数器,每个任务执行完计数器 -1,直到计数器减为 0 时,说明所有的任务都执行完了,就可以执行下一段业务的代码了。

     // 创建线程池
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 20,
    	0, TimeUnit.SECONDS, new LinkedBlockingDeque<>(1024));
    final int taskCount = 5;    // 任务总数
    // 初始化
    CountDownLatch countDownLatch = new CountDownLatch(taskCount); 



     // 增加子线程任务
        ......
        .......// 子线程执行完,计数器 -1。countDownLatch.countDown(); 





    // 阻塞等待线程池任务执行完
    countDownLatch.await();  
方式4:CyclicBarrier 

CyclicBarrier 和 CountDownLatch 类似

// 创建线程池
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 20,
    	0, TimeUnit.SECONDS, new LinkedBlockingDeque<>(1024));
    final int taskCount = 5;    // 任务总数

    // 循环计数器 
    CyclicBarrier cyclicBarrier = new CyclicBarrier(taskCount, new Runnable() {
        @Override
        public void run() {
            // 线程池执行完
            System.out.println();
            System.out.println("线程池所有任务已执行完!");
        }
    });
    // 添加子线程
      .....
      .....// 子线程执行完后,cyclicBarrier.await(); 

 2.1.4 获取子线程返回值(阻塞)

        这种方式就是主线程提交子线程后,子线程异步运行。然后通过阻塞主线程,等待子线程都运行结束,再获取子线程的结果

方式1: 线程池awaitTermination轮询

这种方式只能通过future.get方式获取返回值,只能等全部子线程执行完成才能获取,通过future.get返回值。

  /**
     * 线程池等待,阻塞回到主进程--awaitTermination轮训
     */
    private static void executorsAwaitTermination() {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        Future<String> future = executorService.submit(() -> {
            threadMethod(5, "线程--1");
            return "第一线程";
        });
        Future<String> future2 = executorService.submit(() -> {
            threadMethod(2, "线程--2");
            return "第二线程";
        });
        executorService.shutdown();
        try {
            while (!executorService.awaitTermination(1, TimeUnit.SECONDS)) {
                Util.printfLog("等待中");
            }
        } catch (Exception e) {
        }
    }

方式2:线程池future.get

直接使用future.get获取子线程的返回值,通过遍历集合中的future,以固定的顺序获取子线程返回值,只有当获取的子线程有返回值之后才继续循环获取下一个子线程的返回值。

 /**
     * 线程池等待,阻塞回到主进程--Future.get()
     */
    private static void executorsFutureGet() {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        List<Future<String>> listTemp = new ArrayList<>();
        Future<String> future = executorService.submit(() -> {
            threadMethod(5, "线程--1");
            return "第一线程";
        });
        listTemp.add(future);
        Future<String> future2 = executorService.submit(() -> {
            threadMethod(2, "线程--2");
            return "第二线程";
        });
        listTemp.add(future2);
        for (Future<String> item : listTemp) {
            try {
                System.out.println(item.get());
            } catch (Exception e) {
            }
        }
        executorService.shutdown();
    }

方式3:CountDownLatch类的await(推荐)

通过await方法阻塞主线程等待全部子线程执行完成后,通过future.get获取子线程的返回值,这种方式比方式一更加优雅。

    /**
     * 线程池等待,阻塞回到主进程--CountDownLatch
     */
    private static void executorsCountDownLatch() {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        CountDownLatch countDownLatch = new CountDownLatch(2);
        Future<String> future = executorService.submit(() -> {
            threadMethod(5, "线程--1");
            countDownLatch.countDown();
            return "第一线程";
        });
        Future<String> future2 = executorService.submit(() -> {
            threadMethod(2, "线程--2");
            countDownLatch.countDown();
            return "第二线程";
        });
        executorService.shutdown();
        try {
            countDownLatch.await();
            Util.printfLog(future2.get());
            Util.printfLog(future.get());
        } catch (Exception e) {

        }
    }

方式4:线程池invokeAll 

这种方式和方式二类似,也是通过future.get获取子线程返回值,只能通过遍历集合中的future,以固定的顺序获取子线程返回值。

/**
     * 线程池等待,阻塞回到主进程---使用invokeAll
     */
    private static void executorsInvokeAll() {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        Callable<String> callable = () -> {
            threadMethod(5, "线程--1");
            return "第一线程";
        };
        Callable<String> callable2 = () -> {
            threadMethod(2, "线程--2");
            return "第二线程";
        };
        List<Callable<String>> list = Arrays.asList(callable, callable2);
        try {
            List<Future<String>> futureList = executorService.invokeAll(list);
            executorService.shutdown();
            Util.printfLog("获取结果中---");
            for (Future<String> item : futureList) {
                Util.printfLog(item.get());
            }
        } catch (Exception e) {
        }
    }

方式5: ExecutorCompletionService(强烈推荐)
  • 优雅的获取子线程返回值,只要任何子线程结束就有返回值。
  • ExecutorCompletionService内部管理者一个已完成任务的阻塞队列
  • ExecutorCompletionService引用了一个Executor, 用来执行任务
  • submit()方法最终会委托给内部的executor去执行任务
  • take/poll方法的工作都委托给内部的已完成任务阻塞队列
  • 如果阻塞队列中有已完成的任务, take方法就返回任务的结果, 否则阻塞等待任务完成。
/**
     * 线程池等待,阻塞回到主进程---使用ExecutorCompletionService
     */
    private static void executorsExecutorCompletionService() {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        ExecutorCompletionService<String> executorCompletionService = new ExecutorCompletionService(executorService);

        executorCompletionService.submit(() -> {
            threadMethod(5, "线程--1");
            return "第一线程返回";
        });
        executorCompletionService.submit(() -> {
            threadMethod(2, "线程--2");
            return "第二线程返回";
        });
        executorService.shutdown();
        try {
            Util.printfLog(executorCompletionService.take().get());
            Util.printfLog(executorCompletionService.take().get());
        } catch (Exception e) {
        }

    }

2.1.5 获取子线程返回值(非阻塞)

方式1:新线程Future.get

        JDK 5引入了Future模式。Future接口是Java多线程Future模式的实现,在java.util.concurrent包中,可以来进行异步计算。Future模式是多线程设计常用的一种设计模式。
        Future虽然可以实现获取异步执行结果的需求,但它没有提供通知的机制,我们无法得知Future什么时候完成,不是真正意义上的异步。
        使用Future获得异步执行结果时,要么调用阻塞方法get(),要么轮询看isDone()是否为true,这两种方式都会使主线程也会被迫等待,耗费CPU的资源。

private static void futureGet() {
        try {
            ExecutorService executor = Executors.newFixedThreadPool(1);
            Future<Integer> future = executor.submit(new Callable<Integer>() {
                @Override
                public Integer call() throws Exception {

                    Util.printfLog("===task start===");
                    Util.mySleep(5000);
                    Util.printfLog("===task finish===");
                    return 3;
                }
            });
            executor.shutdown();
            //这里需要返回值时会阻塞主线程,可以再使用一个线程获取future的返回值
            Integer result = future.get();
            Util.printfLog("线程返回值:" + result);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

 

方式2:CompletableFuture类:

        CompletableFuture实现异步操作,加上对lambda的支持,可以说实现异步任务已经发挥到了极致。CompletableFuture弥补了Future模式的缺点。在异步的任务完成后,需要用其结果继续操作时,无需等待。可以直接通过thenAccept、thenApply、thenCompose等方式将前面异步处理的结果交给另外一个异步事件处理线程来处理。

CompletableFuture的静态工厂方法:

方法名描述
runAsync(Runnable runnable)使用ForkJoinPool.commonPool()作为它的线程池执行异步代码,异步操作无返回值
runAsync(Runnable runnable, Executor executor)使用指定的thread pool执行异步代码,异步操作无返回值
supplyAsync(Supplier<U> supplier)使用ForkJoinPool.commonPool()作为它的线程池执行异步代码,异步操作有返回值
supplyAsync(Supplier<U> supplier, Executor executor)使用指定的thread pool执行异步代码,异步操作有返回值
 /**
     * 线程提交后,不阻塞主进程,
     * completableFuture子线程执行完后回调--thread pool执行异步代码
     */
    private static void completableFutureThreadPool() {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
            threadMethod(5, "线程--1");
            return "第一线程";
        }, executorService);
        CompletableFuture<String> completableFuture2 = CompletableFuture.supplyAsync(() -> {
            threadMethod(2, "线程--2");
            return "第二线程";
        }, executorService);
        executorService.shutdown();
        completableFuture.thenAccept((r) -> {
            Util.printfLog(r);
        });
        completableFuture2.thenAccept((r) -> {
            Util.printfLog(r);
        });
    }

2.3 方式3 ForkJoinPool(并行计算java8+)

        Java 8中加入了并行流,从此我们有了一个并行处理集合的简单方法。它和lambda一起,构成了并发计算的一个强大工具。默认情况下是通过ForkJoinPool.commonPool()实现并行的。这个通用池由JVM来管理,并且被JVM进程内的所有线程共享。

    /**
     * 使用ForkJoinPool,异步发处理
     */
    private static void forkJoinPool() {
        List<Supplier<String>> actionList = Arrays.asList(() -> {
                    threadMethod(5, "线程--1");
                    return "第一线程";
                },
                () -> {
                    threadMethod(2, "线程--2");
                    return "线程2返回";
                });
        List<String> threadResult = actionList.parallelStream().map(row -> row.get()).collect(Collectors.toList());
        Util.printfLog(threadResult.stream().collect(Collectors.joining(",")));
    }

2.4 方式4 Spring的Async注解

spring实现异步需要开启注解@EnableAsync,可以使用xml方式或者java code config的方式。
 (1)@Async 异步的方法

4. 总结:

        虽然Thread可以创建线程,但是线程的创建销毁不能很好的控制,就会导致资源耗尽的风险,所以线程资源尽量通过线程池提供,不在应用中自行显示的创建线程,一方面是线程的创建更加规范,可以合理控制开辟线程的数量;另一方面线程的细节管理交给线程池处理,优化了资源的开销。

        线程池的创建尽量不使用Executors,而要通过ThreadPoolExecutor方式,这一方面是由于jdk中Executor框架虽然提供了如newFixedThreadPool()、newSingleThreadExecutor()、newCachedThreadPool()等创建线程池的方法,但都有其局限性,不够灵活;另外由于前面几种方法内部也是通过ThreadPoolExecutor方式实现,使用ThreadPoolExecutor有助于大家明确线程池的运行规则,创建符合自己的业务场景需要的线程池,避免资源耗尽的风险。

5. Executors创建线程池的隐患

Executors 类提供了一系列工厂方法来创建不同类型的线程池,使用起来非常方便,但是它创建按的方式有一些隐患。Executors 创建线程池可能会带来以下几个方面的问题:

  • newFixedThreadPool 和 newSingleThreadExecutor                                                                这两个方法在创建线程池时使用的是 LinkedBlockingQueue 作为任务队列,这个队列的默认大小为 Integer.MAX_VALUE。如果任务提交速度远高于处理速度,队列会不断堆积任务,最终导致内存溢出(OOM)。

  • newCachedThreadPool 和 newScheduledThreadPool
    这两个方法默认线程数 Integer.MAX_VALUE,如果任务量激增,会创建大量线程,导致系统资源耗尽。

  • 除此之外,使用 Executors 创建的线程池,在线程管理和拒绝策略的使用上也不够灵活。

6.线程池正确的创建方式

阿里巴巴开发手册建议使用 ThreadPoolExecutor 来手动创建线程池,以便根据具体的业务需求灵活配置线程池的参数。

/**
corePoolSize: 核心线程数
maximumPoolSize: 最大线程数
keepAliveTime:线程空闲存活时间
timeUnit: 线程空闲存活时间单位
workQueue: // 任务队列
threadFactory: 线程工厂
handler: 拒绝策略
**/
public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit timeUnit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
 

1.​corePoolSize & maximumPoolSize

核心线程数(corePoolSize)和最大线程数(maximumPoolSize)是线程池中非常重要的两个概念。当新任务提交时:

  1. 如果当前运行线程数 < corePoolSize,即使有空闲线程,也会创建新线程执行任务。
  2. 若当前运行线程数 ≥ corePoolSize,任务先进【队列等待】。
  3. 只有队列已满 & 当前运行线程数 < maximumPoolSize,才会新建线程。

2. keepAliveTime & unit

keepAliveTime 为超过 corePoolSize 线程数量的线程最大空闲时间;
unit 为时间单位。


3.等待队列(workQueue)

用于存放等待执行任务的队列。当前运行的线程数量超过 corePoolSize 时,新任务会被放入该队列中等待执行。

三种通用的入队策略:

1. 无界队列(LinkedBlockingQueue):   

特点:队列容量无限(Integer.MAX_VALUE),任务可以一直添加到队列中。也因此,不会触发 maximumPoolSize。
适用场景:适用于任务量大但不希望频繁创建线程的场景,比如 Tomcat。
注意事项:如果任务提交速度远高于处理速度,队列会无限增长,最终导致内存溢出(OOM)。                  

2. 有界队列 (ArrayBlockingQueue):

特点:队列容量固定,任务数超过容量时会触发创建新线程(如果线程数未达最大线程数)。
适用场景:适用于控制并发量,避免资源耗尽。
注意事项:队列容量需要根据业务需求合理设置,过小可能导致频繁触发拒绝策略。

3. 同步队列 (SynchronousQueue):

特点:不存储任务,直接将任务交给空闲线程处理;如果没有空闲线程,则创建新线程(如果线程数未达最大线程数)。
适用场景:任务量较小且需要快速响应的场景,例如高并发短任务处理。
注意事项:线程不足时,任务可能被拒绝。


4.线程工厂 (threadFactory)

用于创建线程的工厂类,通过它可以自定义线程的创建方式,如设置线程的名称、优先级、是否为守护线程等。


5.拒绝策略 (handler)

当任务队列已满且线程数量达到 maximumPoolSize 时,新提交的任务会被拒绝。

四种常见的拒绝策略:

  1. AbortPolicy(默认):直接抛出 RejectedExecutionException 异常。
  2. CallerRunsPolicy:由提交任务的线程直接执行该任务。
  3. DiscardPolicy:直接丢弃新任务,不抛异常。
  4. DiscardOldestPolicy:丢弃任务队列中最老的任务,然后尝试重新提交被拒绝的任务。

参考:

Java 并发的四种风味:Thread、Executor、ForkJoin 和 Actor

java多线程并发之旅-28-Executor CompletionService ExecutorCompletionService 详解

java线程池ThreadPoolExecutor类使用详解

https://zhuanlan.zhihu.com/p/23925671645

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值