java.util.concurrent并发工具包-------2

13. 栅栏 CyclicBarrier

java.util.concurrent.CyclicBarrier 类是一种同步机制,它能够对处理一些算法的线程实现同步。换句话讲,它就是一个所有线程必须等待的一个栅栏,直到所有线程都到达这里,然后所有线程才可以继续做其他事情。图示如下:

java.util.concurrent并发工具包-------2 - cyxinda - JAVA技术分享
 
两个线程在栅栏旁等待对方。
通过调用 CyclicBarrier 对象的 await() 方法,两个线程可以实现互相等待。一旦 N 个线程在等待 CyclicBarrier 达成,所有线程将被释放掉去继续运行。

创建一个 CyclicBarrier

在创建一个 CyclicBarrier 的时候你需要定义有多少线程在被释放之前等待栅栏。创建 CyclicBarrier 示例:
[java]  view plain  copy
 print ?
  1. CyclicBarrier barrier = new CyclicBarrier(2);  

等待一个 CyclicBarrier

以下演示了如何让一个线程等待一个 CyclicBarrier:
[java]  view plain  copy
 print ?
  1. barrier.await();  

当然,你也可以为等待线程设定一个超时时间。等待超过了超时时间之后,即便还没有达成 N 个线程等待 CyclicBarrier 的条件,该线程也会被释放出来。以下是定义超时时间示例:
[java]  view plain  copy
 print ?
  1. barrier.await(10, TimeUnit.SECONDS);  

满足以下任何条件都可以让等待 CyclicBarrier 的线程释放:
  • 最后一个线程也到达 CyclicBarrier(调用 await())
  • 当前线程被其他线程打断(其他线程调用了这个线程的 interrupt() 方法)
  • 其他等待栅栏的线程被打断
  • 其他等待栅栏的线程因超时而被释放
  • 外部线程调用了栅栏的 CyclicBarrier.reset() 方法

CyclicBarrier 行动

CyclicBarrier 支持一个栅栏行动,栅栏行动是一个 Runnable 实例,一旦最后等待栅栏的线程抵达,该实例将被执行。你可以在 CyclicBarrier 的构造方法中将 Runnable 栅栏行动传给它:
[java]  view plain  copy
 print ?
  1. Runnable      barrierAction = ... ;  
  2. CyclicBarrier barrier       = new CyclicBarrier(2, barrierAction);  

CyclicBarrier 示例

以下代码演示了如何使用 CyclicBarrier:
[java]  view plain  copy
 print ?
  1. Runnable barrier1Action = new Runnable() {  
  2.     public void run() {  
  3.         System.out.println("BarrierAction 1 executed ");  
  4.     }  
  5. };  
  6. Runnable barrier2Action = new Runnable() {  
  7.     public void run() {  
  8.         System.out.println("BarrierAction 2 executed ");  
  9.     }  
  10. };  
  11.   
  12. CyclicBarrier barrier1 = new CyclicBarrier(2, barrier1Action);  
  13. CyclicBarrier barrier2 = new CyclicBarrier(2, barrier2Action);  
  14.   
  15. CyclicBarrierRunnable barrierRunnable1 =  
  16.         new CyclicBarrierRunnable(barrier1, barrier2);  
  17.   
  18. CyclicBarrierRunnable barrierRunnable2 =  
  19.         new CyclicBarrierRunnable(barrier1, barrier2);  
  20.   
  21. new Thread(barrierRunnable1).start();  
  22. new Thread(barrierRunnable2).start();  

CyclicBarrierRunnable 类:
[java]  view plain  copy
 print ?
  1. public class CyclicBarrierRunnable implements Runnable{  
  2.   
  3.     CyclicBarrier barrier1 = null;  
  4.     CyclicBarrier barrier2 = null;  
  5.   
  6.     public CyclicBarrierRunnable(  
  7.             CyclicBarrier barrier1,  
  8.             CyclicBarrier barrier2) {  
  9.   
  10.         this.barrier1 = barrier1;  
  11.         this.barrier2 = barrier2;  
  12.     }  
  13.   
  14.     public void run() {  
  15.         try {  
  16.             Thread.sleep(1000);  
  17.             System.out.println(Thread.currentThread().getName() +  
  18.                                 " waiting at barrier 1");  
  19.             this.barrier1.await();  
  20.   
  21.             Thread.sleep(1000);  
  22.             System.out.println(Thread.currentThread().getName() +  
  23.                                 " waiting at barrier 2");  
  24.             this.barrier2.await();  
  25.   
  26.             System.out.println(Thread.currentThread().getName() +  
  27.                                 " done!");  
  28.   
  29.         } catch (InterruptedException e) {  
  30.             e.printStackTrace();  
  31.         } catch (BrokenBarrierException e) {  
  32.             e.printStackTrace();  
  33.         }  
  34.     }  
  35. }  

以上代码控制台输出如下。注意每个线程写入控制台的时序可能会跟你实际执行不一样。比如有时 Thread-0 先打印,有时 Thread-1 先打印。
Thread-0 waiting at barrier 1
Thread-1 waiting at barrier 1
BarrierAction 1 executed
Thread-1 waiting at barrier 2
Thread-0 waiting at barrier 2
BarrierAction 2 executed
Thread-0 done!

Thread-1 done!



14. 交换机 Exchanger

java.util.concurrent.Exchanger 类表示一种两个线程可以进行互相交换对象的会和点。这种机制图示如下:

java.util.concurrent并发工具包-------2 - cyxinda - JAVA技术分享
 
两个线程通过一个 Exchanger 交换对象。
交换对象的动作由 Exchanger 的两个 exchange() 方法的其中一个完成。以下是一个示例:
[java]  view plain  copy
 print ?
  1. Exchanger exchanger = new Exchanger();  
  2.   
  3. ExchangerRunnable exchangerRunnable1 =  
  4.         new ExchangerRunnable(exchanger, "A");  
  5.   
  6. ExchangerRunnable exchangerRunnable2 =  
  7.         new ExchangerRunnable(exchanger, "B");  
  8.   
  9. new Thread(exchangerRunnable1).start();  
  10. new Thread(exchangerRunnable2).start();  
ExchangerRunnable 代码:
[java]  view plain  copy
 print ?
  1. public class ExchangerRunnable implements Runnable{  
  2.   
  3.     Exchanger exchanger = null;  
  4.     Object    object    = null;  
  5.   
  6.     public ExchangerRunnable(Exchanger exchanger, Object object) {  
  7.         this.exchanger = exchanger;  
  8.         this.object = object;  
  9.     }  
  10.   
  11.     public void run() {  
  12.         try {  
  13.             Object previous = this.object;  
  14.   
  15.             this.object = this.exchanger.exchange(this.object);  
  16.   
  17.             System.out.println(  
  18.                     Thread.currentThread().getName() +  
  19.                     " exchanged " + previous + " for " + this.object  
  20.             );  
  21.         } catch (InterruptedException e) {  
  22.             e.printStackTrace();  
  23.         }  
  24.     }  
  25. }  

以上程序输出:
Thread-0 exchanged A for B

Thread-1 exchanged B for A



15. 信号量 Semaphore

java.util.concurrent.Semaphore 类是一个计数信号量。这就意味着它具备两个主要方法:
  • acquire()
  • release()
计数信号量由一个指定数量的 "许可" 初始化。每调用一次 acquire(),一个许可会被调用线程取走。每调用一次 release(),一个许可会被返还给信号量。因此,在没有任何 release() 调用时,最多有 N 个线程能够通过 acquire() 方法,N 是该信号量初始化时的许可的指定数量。这些许可只是一个简单的计数器。这里没啥奇特的地方。

Semaphore 用法

信号量主要有两种用途:
  1. 保护一个重要(代码)部分防止一次超过 N 个线程进入。
  2. 在两个线程之间发送信号。

保护重要部分

如果你将信号量用于保护一个重要部分,试图进入这一部分的代码通常会首先尝试获得一个许可,然后才能进入重要部分(代码块),执行完之后,再把许可释放掉。比如这样:

[java]  view plain  copy
 print ?
  1. Semaphore semaphore = new Semaphore(1);  
  2.   
  3. //critical section  
  4. semaphore.acquire();  
  5.   
  6. ...  
  7.   
  8. semaphore.release();  

在线程之间发送信号

如果你将一个信号量用于在两个线程之间传送信号,通常你应该用一个线程调用 acquire() 方法,而另一个线程调用 release() 方法。
如果没有可用的许可,acquire() 调用将会阻塞,直到一个许可被另一个线程释放出来。同理,如果无法往信号量释放更多许可时,一个 release() 调用也会阻塞。
通过这个可以对多个线程进行协调。比如,如果线程 1 将一个对象插入到了一个共享列表(list)之后之后调用了 acquire(),而线程 2 则在从该列表中获取一个对象之前调用了 release(),这时你其实已经创建了一个阻塞队列。信号量中可用的许可的数量也就等同于该阻塞队列能够持有的元素个数。

公平

没有办法保证线程能够公平地可从信号量中获得许可。也就是说,无法担保掉第一个调用 acquire() 的线程会是第一个获得一个许可的线程。如果第一个线程在等待一个许可时发生阻塞,而第二个线程前来索要一个许可的时候刚好有一个许可被释放出来,那么它就可能会在第一个线程之前获得许可。
如果你想要强制公平,Semaphore 类有一个具有一个布尔类型的参数的构造子,通过这个参数以告知 Semaphore 是否要强制公平。强制公平会影响到并发性能,所以除非你确实需要它否则不要启用它。
以下是如何在公平模式创建一个 Semaphore 的示例:
[java]  view plain  copy
 print ?
  1. Semaphore semaphore = new Semaphore(1true);  

更多方法

java.util.concurrent.Semaphore 类还有很多方法,比如:
  • availablePermits()
  • acquireUninterruptibly()
  • drainPermits()
  • hasQueuedThreads()
  • getQueuedThreads()
  • tryAcquire()
  • 等等

这些方法的细节请参考 Java 文档。



16. 执行器服务 ExecutorService

java.util.concurrent.ExecutorService 接口表示一个异步执行机制,使我们能够在后台执行任务。因此一个 ExecutorService 很类似于一个线程池。实际上,存在于 java.util.concurrent 包里的 ExecutorService 实现就是一个线程池实现。

ExecutorService 例子

以下是一个简单的 ExecutorService 例子:
[java]  view plain  copy
 print ?
  1. ExecutorService executorService = Executors.newFixedThreadPool(10);  
  2.   
  3. executorService.execute(new Runnable() {  
  4.     public void run() {  
  5.         System.out.println("Asynchronous task");  
  6.     }  
  7. });  
  8.   
  9. executorService.shutdown();  

首先使用 newFixedThreadPool() 工厂方法创建一个 ExecutorService。这里创建了一个十个线程执行任务的线程池。
然后,将一个 Runnable 接口的匿名实现类传递给 execute() 方法。这将导致 ExecutorService 中的某个线程执行该 Runnable。

任务委派

下图说明了一个线程是如何将一个任务委托给一个 ExecutorService 去异步执行的:

java.util.concurrent并发工具包-------2 - cyxinda - JAVA技术分享
 
一个线程将一个任务委派给一个 ExecutorService 去异步执行。
一旦该线程将任务委派给 ExecutorService,该线程将继续它自己的执行,独立于该任务的执行。

ExecutorService 实现

既然 ExecutorService 是个接口,如果你想用它的话就得去使用它的实现类之一。java.util.concurrent 包提供了 ExecutorService 接口的以下实现类:

创建一个 ExecutorService

ExecutorService 的创建依赖于你使用的具体实现。但是你也可以使用 Executors 工厂类来创建 ExecutorService 实例。以下是几个创建 ExecutorService 实例的例子:
[java]  view plain  copy
 print ?
  1. ExecutorService executorService1 = Executors.newSingleThreadExecutor();  
  2.   
  3. ExecutorService executorService2 = Executors.newFixedThreadPool(10);  
  4.   
  5. ExecutorService executorService3 = Executors.newScheduledThreadPool(10);  

ExecutorService 使用

有几种不同的方式来将任务委托给 ExecutorService 去执行:
  • execute(Runnable)
  • submit(Runnable)
  • submit(Callable)
  • invokeAny(...)
  • invokeAll(...)
接下来我们挨个看一下这些方法。

execute(Runnable)

execute(Runnable) 方法要求一个 java.lang.Runnable 对象,然后对它进行异步执行。以下是使用 ExecutorService 执行一个 Runnable 的示例:
[java]  view plain  copy
 print ?
  1. ExecutorService executorService = Executors.newSingleThreadExecutor();  
  2.   
  3. executorService.execute(new Runnable() {  
  4.     public void run() {  
  5.         System.out.println("Asynchronous task");  
  6.     }  
  7. });  
  8.   
  9. executorService.shutdown();  

没有办法得知被执行的 Runnable 的执行结果。如果有需要的话你得使用一个 Callable(以下将做介绍)。

submit(Runnable)

submit(Runnable) 方法也要求一个 Runnable 实现类,但它返回一个 Future 对象。这个 Future 对象可以用来检查 Runnable 是否已经执行完毕。
以下是 ExecutorService submit() 示例:
[java]  view plain  copy
 print ?
  1. Future future = executorService.submit(new Runnable() {  
  2.     public void run() {  
  3.         System.out.println("Asynchronous task");  
  4.     }  
  5. });  
  6.   
  7. future.get();  //returns null if the task has finished correctly.  

submit(Callable)

submit(Callable) 方法类似于 submit(Runnable) 方法,除了它所要求的参数类型之外。Callable 实例除了它的 call() 方法能够返回一个结果之外和一个 Runnable 很相像。Runnable.run() 不能够返回一个结果。
Callable 的结果可以通过 submit(Callable) 方法返回的 Future 对象进行获取。以下是一个 ExecutorService Callable 示例:
[java]  view plain  copy
 print ?
  1. Future future = executorService.submit(new Callable(){  
  2.     public Object call() throws Exception {  
  3.         System.out.println("Asynchronous Callable");  
  4.         return "Callable Result";  
  5.     }  
  6. });  
  7.   
  8. System.out.println("future.get() = " + future.get());  

以上代码输出:
Asynchronous Callable
future.get() = Callable Result

invokeAny()

invokeAny() 方法要求一系列的 Callable 或者其子接口的实例对象。调用这个方法并不会返回一个 Future,但它返回其中一个 Callable 对象的结果。无法保证返回的是哪个 Callable 的结果 - 只能表明其中一个已执行结束。
如果其中一个任务执行结束(或者抛了一个异常),其他 Callable 将被取消。
以下是示例代码:
[java]  view plain  copy
 print ?
  1. ExecutorService executorService = Executors.newSingleThreadExecutor();  
  2.   
  3. Set<Callable<String>> callables = new HashSet<Callable<String>>();  
  4.   
  5. callables.add(new Callable<String>() {  
  6.     public String call() throws Exception {  
  7.         return "Task 1";  
  8.     }  
  9. });  
  10. callables.add(new Callable<String>() {  
  11.     public String call() throws Exception {  
  12.         return "Task 2";  
  13.     }  
  14. });  
  15. callables.add(new Callable<String>() {  
  16.     public String call() throws Exception {  
  17.         return "Task 3";  
  18.     }  
  19. });  
  20.   
  21. String result = executorService.invokeAny(callables);  
  22.   
  23. System.out.println("result = " + result);  
  24.   
  25. executorService.shutdown();  

上述代码将会打印出给定 Callable 集合中的一个的执行结果。我自己试着执行了它几次,结果始终在变。有时是 "Task 1",有时是 "Task 2" 等等。

invokeAll()

invokeAll() 方法将调用你在集合中传给 ExecutorService 的所有 Callable 对象。invokeAll() 返回一系列的 Future 对象,通过它们你可以获取每个 Callable 的执行结果。
记住,一个任务可能会由于一个异常而结束,因此它可能没有 "成功"。无法通过一个 Future 对象来告知我们是两种结束中的哪一种。
以下是一个代码示例:
[java]  view plain  copy
 print ?
  1. ExecutorService executorService = Executors.newSingleThreadExecutor();  
  2.   
  3. Set<Callable<String>> callables = new HashSet<Callable<String>>();  
  4.   
  5. callables.add(new Callable<String>() {  
  6.     public String call() throws Exception {  
  7.         return "Task 1";  
  8.     }  
  9. });  
  10. callables.add(new Callable<String>() {  
  11.     public String call() throws Exception {  
  12.         return "Task 2";  
  13.     }  
  14. });  
  15. callables.add(new Callable<String>() {  
  16.     public String call() throws Exception {  
  17.         return "Task 3";  
  18.     }  
  19. });  
  20.   
  21. List<Future<String>> futures = executorService.invokeAll(callables);  
  22.   
  23. for(Future<String> future : futures){  
  24.     System.out.println("future.get = " + future.get());  
  25. }  
  26.   
  27. executorService.shutdown();  

ExecutorService 关闭

使用完 ExecutorService 之后你应该将其关闭,以使其中的线程不再运行。
比如,如果你的应用是通过一个 main() 方法启动的,之后 main 方法退出了你的应用,如果你的应用有一个活动的 ExexutorService 它将还会保持运行。ExecutorService 里的活动线程阻止了 JVM 的关闭。
要终止 ExecutorService 里的线程你需要调用 ExecutorService 的 shutdown() 方法。ExecutorService 并不会立即关闭,但它将不再接受新的任务,而且一旦所有线程都完成了当前任务的时候,ExecutorService 将会关闭。在 shutdown() 被调用之前所有提交给 ExecutorService 的任务都被执行。

如果你想要立即关闭 ExecutorService,你可以调用 shutdownNow() 方法。这样会立即尝试停止所有执行中的任务,并忽略掉那些已提交但尚未开始处理的任务。无法担保执行任务的正确执行。可能它们被停止了,也可能已经执行结束。



17. 线程池执行者 ThreadPoolExecutor

java.util.concurrent.ThreadPoolExecutor 是 ExecutorService 接口的一个实现。ThreadPoolExecutor 使用其内部池中的线程执行给定任务(Callable 或者 Runnable)。
ThreadPoolExecutor 包含的线程池能够包含不同数量的线程。池中线程的数量由以下变量决定:
  • corePoolSize
  • maximumPoolSize
当一个任务委托给线程池时,如果池中线程数量低于 corePoolSize,一个新的线程将被创建,即使池中可能尚有空闲线程。
如果内部任务队列已满,而且有至少 corePoolSize 正在运行,但是运行线程的数量低于 maximumPoolSize,一个新的线程将被创建去执行该任务。
ThreadPoolExecutor 图解:

java.util.concurrent并发工具包-------2 - cyxinda - JAVA技术分享
 
一个 ThreadPoolExecutor

创建一个 ThreadPoolExecutor

ThreadPoolExecutor 有若干个可用构造子。比如:
[java]  view plain  copy
 print ?
  1. int  corePoolSize  =    5;  
  2. int  maxPoolSize   =   10;  
  3. long keepAliveTime = 5000;  
  4.   
  5. ExecutorService threadPoolExecutor =  
  6.         new ThreadPoolExecutor(  
  7.                 corePoolSize,  
  8.                 maxPoolSize,  
  9.                 keepAliveTime,  
  10.                 TimeUnit.MILLISECONDS,  
  11.                 new LinkedBlockingQueue<Runnable>()  
  12.                 );  

但是,除非你确实需要显式为 ThreadPoolExecutor 定义所有参数,使用 java.util.concurrent.Executors 类中的工厂方法之一会更加方便,正如  ExecutorService 小节所述。 



18.  定时执行者服务 ScheduledExecutorService

java.util.concurrent.ScheduledExecutorService 是一个 ExecutorService, 它能够将任务延后执行,或者间隔固定时间多次执行。 任务由一个工作者线程异步执行,而不是由提交任务给 ScheduledExecutorService 的那个线程执行。

ScheduledExecutorService 例子

以下是一个简单的 ScheduledExecutorService 示例:
[java]  view plain  copy
 print ?
  1. ScheduledExecutorService scheduledExecutorService =  
  2.         Executors.newScheduledThreadPool(5);  
  3.   
  4. ScheduledFuture scheduledFuture =  
  5.     scheduledExecutorService.schedule(new Callable() {  
  6.         public Object call() throws Exception {  
  7.             System.out.println("Executed!");  
  8.             return "Called!";  
  9.         }  
  10.     },  
  11.     5,  
  12.     TimeUnit.SECONDS);  

首先一个内置 5 个线程的 ScheduledExecutorService 被创建。之后一个 Callable 接口的匿名类示例被创建然后传递给 schedule() 方法。后边的俩参数定义了 Callable 将在 5 秒钟之后被执行。

ScheduledExecutorService 实现

既然 ScheduledExecutorService 是一个接口,你要用它的话就得使用 java.util.concurrent 包里对它的某个实现类。ScheduledExecutorService 具有以下实现类:
  • ScheduledThreadPoolExecutor

创建一个 ScheduledExecutorService

如何创建一个 ScheduledExecutorService 取决于你采用的它的实现类。但是你也可以使用 Executors 工厂类来创建一个 ScheduledExecutorService 实例。比如:
[java]  view plain  copy
 print ?
  1. ScheduledExecutorService scheduledExecutorService =  
  2.   
  3.         Executors.newScheduledThreadPool(5);  

ScheduledExecutorService 使用

一旦你创建了一个 ScheduledExecutorService,你可以通过调用它的以下方法:
  • schedule (Callable task, long delay, TimeUnit timeunit)
  • schedule (Runnable task, long delay, TimeUnit timeunit)
  • scheduleAtFixedRate (Runnable, long initialDelay, long period, TimeUnit timeunit)
  • scheduleWithFixedDelay (Runnable, long initialDelay, long period, TimeUnit timeunit)
下面我们就简单看一下这些方法。

schedule (Callable task, long delay, TimeUnit timeunit)

这个方法计划指定的 Callable 在给定的延迟之后执行。
这个方法返回一个 ScheduledFuture,通过它你可以在它被执行之前对它进行取消,或者在它执行之后获取结果。
以下是一个示例:
[java]  view plain  copy
 print ?
  1. ScheduledExecutorService scheduledExecutorService =  
  2.         Executors.newScheduledThreadPool(5);  
  3.   
  4. ScheduledFuture scheduledFuture =  
  5.     scheduledExecutorService.schedule(new Callable() {  
  6.         public Object call() throws Exception {  
  7.             System.out.println("Executed!");  
  8.             return "Called!";  
  9.         }  
  10.     },  
  11.     5,  
  12.     TimeUnit.SECONDS);  
  13.   
  14. System.out.println("result = " + scheduledFuture.get());  
  15.   
  16. scheduledExecutorService.shutdown();  

示例输出结果:
Executed!
result = Called!

schedule (Runnable task, long delay, TimeUnit timeunit)

除了 Runnable 无法返回一个结果之外,这一方法工作起来就像以一个 Callable 作为一个参数的那个版本的方法一样,因此 ScheduledFuture.get() 在任务执行结束之后返回 null。

scheduleAtFixedRate (Runnable, long initialDelay, long period, TimeUnit timeunit)

这一方法规划一个任务将被定期执行。该任务将会在首个 initialDelay 之后得到执行,然后每个 period 时间之后重复执行。
如果给定任务的执行抛出了异常,该任务将不再执行。如果没有任何异常的话,这个任务将会持续循环执行到 ScheduledExecutorService 被关闭。
如果一个任务占用了比计划的时间间隔更长的时候,下一次执行将在当前执行结束执行才开始。计划任务在同一时间不会有多个线程同时执行。

scheduleWithFixedDelay (Runnable, long initialDelay, long period, TimeUnit timeunit)

除了 period 有不同的解释之外这个方法和 scheduleAtFixedRate() 非常像。
scheduleAtFixedRate() 方法中,period 被解释为前一个执行的开始和下一个执行的开始之间的间隔时间。
而在本方法中,period 则被解释为前一个执行的结束和下一个执行的结束之间的间隔。因此这个延迟是执行结束之间的间隔,而不是执行开始之间的间隔。

ScheduledExecutorService 关闭

正如 ExecutorService,在你使用结束之后你需要把 ScheduledExecutorService 关闭掉。否则他将导致 JVM 继续运行,即使所有其他线程已经全被关闭。

你可以使用从 ExecutorService 接口继承来的 shutdown() 或 shutdownNow() 方法将 ScheduledExecutorService 关闭。参见 ExecutorService 关闭部分以获取更多信息。


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值