线程池源码分析

线程池有很多知识点,从源码出发更脚踏实地,此外,源码注释比起网上的讲解更详尽、表述更准确,所以有必要回顾一期源码:

来了一个任务,如果线程个数没有达到核心线程数,就会创建线程来执行任务;如果达到核心线程数,就会把任务放到任务队列中;如果任务队列满了,线程个数没有达到最大线程数,就会创建线程来执行任务;如果达到最大线程数了,就会执行拒绝策略。

线程池的目标是线程复用。线程执行第一个任务后,在Runnable里循环获取任务。线程个数大于核心线程数,线程是空闲的,超过保活时间,就会退出循环。

除了线程达到最大线程数并且任务队列满,线程状态是关闭、停止、整理、终止,也会执行拒绝策略。关闭状态下不能接收新任务,会继续处理旧任务。停止状态下不能接收新任务,中断正在执行的任务。关闭和停止状态下清空任务后,就会成为整理状态。整理状态下执行ThreadPoolExecutor的terminated方法,就会成为终止状态。

拒绝策略就是RejectedExecutionHandler的rejectedExecution方法。

submit(callable)时,会将callable传给futureTask,执行execute(futureTask)。FutureTask实现了Runnable接口。FutureTask的run方法里会调Callable的call方法。

如果是CPU密集型,核心线程数应该是CPU核心数,最大线程数可以是核心线程数+1,避免线程频繁切换带来的开销。如果是IO密集型,核心线程数可以是CPU核心数的两倍,可以用公式 (线程保活时间+线程CPU时间)/线程CPU时间*CPU核心数。CPU密集型是指逻辑运算占比大。IO密集型是指IO占比大。

public ThreadPoolExecutor(int corePoolSize, //核心线程数量
 int maximumPoolSize, //最大线程数
 long keepAliveTime, //空闲线程的存活时间,超过空闲时间上限时线程就结束了,默认是针对线程数大于最大线程数时,allowCoreThreadTimeOut(true)时对所有线程有效
 TimeUnit unit, //存活时间单位
 BlockingQueue<Runnable> workQueue, //保存执行任务的队列,即任务队列
ThreadFactory threadFactory,//创建新线程使用的工厂
RejectedExecutionHandler handler //当任务无法执行的时候的处理方式)

JDK提供了几种,但不推荐直接使用,应该按照实际需求设置参数。阿里Java手册也是这样要求的。 

FixedThreadPool:

核心线程数等于最大线程数,保活时间为0,队列为无界队列(队列大小Integer.MAX_VALUE,一般用不到大小上限,所以习惯上称无界队列)。

线程池中的线程数上限固定,采用无界队列。适合任务量比较固定,任务比较耗时的场景。

/ * *
*创建重用固定数量线程的线程池
*操作一个共享的无界队列,使用提供的
* ThreadFactory在需要时创建新线程。在任何时候,
*大多数{@code nThreads}线程将被积极处理
*任务。当所有线程都被提交时,是否有其他任务被提交
*活动时,它们将在队列中等待,直到有一个线程是活动的
*可用。如果任何线程在运行期间由于失败而终止
*在关闭之前执行,一个新的将取代它,如果
*需要执行后续任务。池中的线程会
*存在,直到显式{@link ExecutorService#shutdown
*关闭}。
*
线程池中的线程数
创建新线程时要使用的线程工厂
返回新创建的线程池
* @抛出NullPointerException,如果threadFactory是空的
* @抛出IllegalArgumentException如果{@code nThreads <= 0}
* /
    public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>(),
                                      threadFactory);
    }

SingleThreadExecutor:

最多有一个线程, 采用无界队列。适合任务依次执行,任何时候,只有一个任务执行的场景。

/ * *
*创建使用单个工作线程操作的执行器
*关闭一个无界队列。(注意,如果这个单
*线程在执行之前由于失败而终止
*关机时,如果需要执行,会有一个新的替换
*后续任务。)任务被保证执行
*按顺序排列,任何活动的任务不超过一个
*。不像其他等价的
* {@code newFixedThreadPool(1)
*保证不被重新配置使用额外的线程。
*
返回新创建的单线程执行器
* /
    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

/ * *
*创建使用单个工作线程操作的执行器
*关闭一个未绑定队列,并使用所提供的线程工厂来
*在需要时创建一个新线程。不同于其他
*等效的{@code newFixedThreadPool(1, threadFactory)
*返回的执行器保证不会被重新配置使用
*额外的线程。
*
* @param threadFactory创建新线程时要使用的工厂
*线程
*
返回新创建的单线程执行器
* @抛出NullPointerException,如果threadFactory是空的
* /
    public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>(),
                                    threadFactory));
    }

CachedThreadPool:

线程最多Integer.MAX_VALUE个,采用同步队列。所有线程超过60秒空闲就结束。使用同步队列,任何时候只能容纳一个任务,队列里的任务不被移除,下一个任务不能被添加。适合执行许多短期任务的场景。

/ * *
*创建一个线程池,根据需要创建新的线程,但是
*将重用之前构建的线程
*可用。这些池通常会提高性能
执行许多短期的异步任务的程序。
*调用{@code execute}将重用之前构造的
*线程(如果可用)。如果没有可用的线程,则使用新线程
*线程将被创建并添加到池中。线程,
*未使用60秒将终止并删除
*缓存。因此,一个足够长时间保持空闲的池将会
*不消耗任何资源。注意,有类似的池
*属性,但不同的细节(例如,超时参数)
*可以使用{@link ThreadPoolExecutor}构造函数创建。
*
返回新创建的线程池
* /
    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

/ * *
*创建一个线程池,根据需要创建新的线程,但是
*将重用之前构建的线程
*可用,并使用所提供的
* ThreadFactory在需要时创建新线程。
创建新线程时要使用的线程工厂
返回新创建的线程池
* @抛出NullPointerException,如果threadFactory是空的
* /
    public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>(),
                                      threadFactory);
    }

ScheduledThreadPool: 

/ * *
*创建一个线程池,可以调度命令在一个线程后运行
*给定延迟,或定期执行。
* @param corePoolSize保存在池中的线程数,
*即使他们空闲
返回一个新创建的计划线程池
* @抛出IllegalArgumentException如果{@code corePoolSize < 0}
* /
    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }

/ * *
*创建一个线程池,可以调度命令在一个线程后运行
*给定延迟,或定期执行。
* @param corePoolSize保存在池中的线程数,
*即使他们空闲
返回一个新创建的计划线程池
* @抛出IllegalArgumentException如果{@code corePoolSize < 0}
* /  
    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }

 WorkStealingPool:

/ * *
*创建一个线程池,维护足够的线程来支持
*给定的并行度级别,可以使用多个队列来实现
*减少争用。平行度级别对应于
*积极参与或可用的最大线程数
参与任务处理。线程的实际数量可能
*动态增长和收缩。偷工减料的人不会
*保证提交任务的顺序
*执行。
*
目标并行度级别
返回新创建的线程池
* @抛出IllegalArgumentException如果{@code parallelism <= 0}
* @since 1.8
* /
    public static ExecutorService newWorkStealingPool(int parallelism) {
        return new ForkJoinPool
            (parallelism,
             ForkJoinPool.defaultForkJoinWorkerThreadFactory,
             null, true);
    }

/ * *
*创建一个工作窃取线程池使用所有
* {@link Runtime#availableProcessors available processors}
*作为其目标并行度级别。
返回新创建的线程池
* @see # newWorkStealingPool (int)
* @since 1.8
* /
    public static ExecutorService newWorkStealingPool() {
        return new ForkJoinPool
            (Runtime.getRuntime().availableProcessors(),
             ForkJoinPool.defaultForkJoinWorkerThreadFactory,
             null, true);
    }

核心源码: 

execute(Runnable command)

    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
/ *
*分3个步骤进行:
*
* 1。如果运行的线程少于corePoolSize,请尝试
*用给定的命令作为第一个线程启动一个新线程
*任务。对addWorker的调用会自动检查runState和
* workerCount,这样可以防止虚假警报的增加
*线程时,它不应该,通过返回false。
*
* 2。如果任务可以成功排队,那么我们仍然需要
*来再次检查我们是否应该添加一个线程
*(因为在上次检查后已有的已经死亡)或那样
*自从进入此方法后池就关闭了。所以我们
*重新检查状态,如有必要回滚队列
*停止,或启动一个新线程,如果没有线程。
*
* 3。如果我们不能将任务放入队列,那么我们尝试添加一个新的
*线程。如果它失败了,我们知道我们被关闭或饱和了
*因此拒绝这项任务。
* /
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }
private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;

            for (;;) {
                int wc = workerCountOf(c);
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                c = ctl.get();  // Re-read ctl
                if (runStateOf(c) != rs)
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }

        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                    // Recheck while holding lock.
                    // Back out on ThreadFactory failure or if
                    // shut down before lock acquired.
                    int rs = runStateOf(ctl.get());

                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        if (t.isAlive()) // precheck that t is startable
                            throw new IllegalThreadStateException();
                        workers.add(w);
                        int s = workers.size();
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                if (workerAdded) {
                    t.start(); // 开启线程
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }

线程包装在Worker中:

    private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable
    {        
        Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }

        /** Delegates main run loop to outer runWorker  */
        public void run() {
            runWorker(this);
        }
    final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
            while (task != null || (task = getTask()) != null) { // 第一个任务不为空或者从队列中拿到的任务不为空
                w.lock();
                // If pool is stopping, ensure thread is interrupted;
                // if not, ensure thread is not interrupted.  This
                // requires a recheck in second case to deal with
                // shutdownNow race while clearing interrupt
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                        task.run(); // 运行任务
                    } catch (RuntimeException x) {
                        thrown = x; throw x;
                    } catch (Error x) {
                        thrown = x; throw x;
                    } catch (Throwable x) {
                        thrown = x; throw new Error(x);
                    } finally {
                        afterExecute(task, thrown);
                    }
                } finally {
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }

 获取队列中的任务:

    private Runnable getTask() {
        boolean timedOut = false; // Did the last poll() time out?

        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                decrementWorkerCount();
                return null;
            }

            int wc = workerCountOf(c);

            // Are workers subject to culling?
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

            if ((wc > maximumPoolSize || (timed && timedOut)) // 线程个数大于最大的池大小(最大线程数)或者线程保活时间(空闲时间)到了,就返回null任务,会导致退出上一层方法的循环,线程结束
                && (wc > 1 || workQueue.isEmpty())) {
                if (compareAndDecrementWorkerCount(c))
                    return null;
                continue;
            }

            try {
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }
    private void processWorkerExit(Worker w, boolean completedAbruptly) {
        if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
            decrementWorkerCount();

        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            completedTaskCount += w.completedTasks;
            workers.remove(w);
        } finally {
            mainLock.unlock();
        }

        tryTerminate();

        int c = ctl.get();
        if (runStateLessThan(c, STOP)) {
            if (!completedAbruptly) {
                int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
                if (min == 0 && ! workQueue.isEmpty())
                    min = 1;
                if (workerCountOf(c) >= min)
                    return; // replacement not needed
            }
            addWorker(null, false);
        }
    }

线程池状态转换: 

/ * *
*主池控制状态ctl是一个原子整数封装,前3位表示线程池状态,后29位表示线程池的线程数量
*两个概念领域
workerCount,表示线程的有效数量
* runState,指示是否运行、关闭等
*
为了将它们打包成一个int,我们将workerCount限制为
*(2^29)-1(大约5亿个)线程,而不是(2^31)-1 (2
*亿)其他可表示的。如果这是一个问题
*将来,变量可以改为AtomicLong,
*和shift/掩模常数调整如下。但直到需要
如果使用int,这段代码会更快、更简单一些。
*
工人总数是已被解雇的工人人数
*允许开始,不允许停止。价值可能是
*暂时不同于活动线程的实际数量,
*例如,当线程工厂无法创建线程时
*询问,退出线程仍在执行
*终止前簿记。用户可见的池大小为
*报告当前工人的大小设置。
*
* runState提供主生命周期控制,取值:
*
*RUNNING:接受新任务并处理已排队的任务
*SHUTDOWN:不接受新任务,但处理已排队的任务
*STOP:不接受新任务,不处理排队的任务,
*和中断正在进行的任务
*TIDYING:所有任务已终止,workerCount为零,
线程转换到状态整理
*将运行terminated()钩子方法
* TERMINATED: TERMINATED()已经完成
*
*这些值之间的数字顺序很重要
*要求比较。运行状态单调递增
*时间,但不需要击中每个状态。转换:
*
*RUNNING -> SHUTDOWN
*    对shutdown()的调用,可能在finalize()中隐式调用
*(RUNNING or SHUTDOWN) -> STOP
*    关于调用shutdownNow()
*SHUTDOWN -> TIDYING
*    当队列和池都为空时
*STOP -> TIDYING
*    当池是空的
*TIDYING -> TERMINATED
*    当terminate()钩子方法完成时
*
*线程等待awaitTermination()将返回当
*状态到达终止。
*
*检测从关机到整理的过渡更少
*比你想的要简单,因为队列可能会变成
*空后非空,反之亦然,关机状态,但
只有在看到它是空的时候,我们才能终止
workerCount为0(有时需要重新检查——请参阅
*以下)。
* /
   // runState is stored in the high-order bits
    private static final int RUNNING    = -1 << COUNT_BITS;
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    private static final int STOP       =  1 << COUNT_BITS;
    private static final int TIDYING    =  2 << COUNT_BITS;
    private static final int TERMINATED =  3 << COUNT_BITS;

RUNNING->SHUTDOWN->TIDYING->TERMINATED

RUNNING->STOP->TIDYING->TERMINATED

shutdown:

    public void shutdown() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            advanceRunState(SHUTDOWN); // 转换为SHUTDOWN状态
            interruptIdleWorkers(); // 中断闲置线程
            onShutdown(); // hook for ScheduledThreadPoolExecutor
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
    }
    private void interruptIdleWorkers() {
        interruptIdleWorkers(false);
    }

 中断空闲线程:

    private void interruptIdleWorkers(boolean onlyOne) {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            for (Worker w : workers) {
                Thread t = w.thread;
                if (!t.isInterrupted() && w.tryLock()) { // 如果线程未被中断
                    try {
                        t.interrupt(); // 中断线程
                    } catch (SecurityException ignore) {
                    } finally {
                        w.unlock();
                    }
                }
                if (onlyOne)
                    break;
            }
        } finally {
            mainLock.unlock();
        }
    }

shutdownNow(): 

    public List<Runnable> shutdownNow() {
        List<Runnable> tasks;
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            advanceRunState(STOP); // 转换为STOP状态
            interruptWorkers(); // 中断所有线程
            tasks = drainQueue(); // 清空任务队列,获得未执行的任务列表
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
        return tasks; // 返回未执行的任务列表
    }
    private List<Runnable> drainQueue() {
        BlockingQueue<Runnable> q = workQueue;
        ArrayList<Runnable> taskList = new ArrayList<Runnable>();
        q.drainTo(taskList);
        if (!q.isEmpty()) {
            for (Runnable r : q.toArray(new Runnable[0])) {
                if (q.remove(r))
                    taskList.add(r);
            }
        }
        return taskList;
    }
/ * *
*转换到终止状态,如果(SHUTDOWN和池
*和队列空)或(STOP和池空)。如果否则
*有资格终止,但workerCount是非零,中断一个
*闲置工人,以确保关闭信号传播。这
方法必须在任何可能发生的操作之后调用
*可能的终止——减少员工数量或取消任务
*从队列关闭时。该方法是非私有的
*允许访问ScheduledThreadPoolExecutor。
* / 
    final void tryTerminate() {
        for (;;) {
            int c = ctl.get();
            if (isRunning(c) ||
                runStateAtLeast(c, TIDYING) ||
                (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
                return;
            if (workerCountOf(c) != 0) { // Eligible to terminate
                interruptIdleWorkers(ONLY_ONE);
                return;
            }

            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) { // 转换为TIDYING状态
                    try {
                        terminated(); // 结束方法
                    } finally {
                        ctl.set(ctlOf(TERMINATED, 0)); // 转换为TERMINATED状态
                        termination.signalAll();
                    }
                    return;
                }
            } finally {
                mainLock.unlock();
            }
            // else retry on failed CAS
        }
    }

拒绝策略:

    final void reject(Runnable command) {
        handler.rejectedExecution(command, this);
    }

默认拒绝策略:

    private static final RejectedExecutionHandler defaultHandler =
        new AbortPolicy();
    public static class AbortPolicy implements RejectedExecutionHandler {
        /**
         * Creates an {@code AbortPolicy}.
         */
        public AbortPolicy() { }

        /**
         * Always throws RejectedExecutionException.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         * @throws RejectedExecutionException always
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            throw new RejectedExecutionException("Task " + r.toString() +
                                                 " rejected from " +
                                                 e.toString());
        }
    }

线程池大小设置:

如果是 CPU 密集型,主要是执行计算任务,响应时间很快,cpu 一直在运行,这种任务 cpu
的利用率很高,那么线程数的配置应该根据 CPU 核心数来决定,CPU 核心数=最大同时执行
线程数,加入 CPU 核心数为 4,那么服务器最多能同时执行 4 个线程。过多的线程会导致上
下文切换反而使得效率降低。那线程池的最大线程数可以配置为 cpu 核心数+1
 
如果是 IO 密集型,主要是进行 IO 操作,执行 IO 操作的时间较长,这是 cpu 出于空闲状态,
导致 cpu 的利用率不高,这种情况下可以增加线程池的大小。这种情况下可以结合线程的等
待时长来做判断,等待时间越高,那么线程数也相对越多。一般可以配置 cpu 核心数的 2 倍。
一个公式:线程池设定最佳线程数目 = ((线程池设定的线程保活时间+线程 CPU 时间)/
线程 CPU 时间 )* CPU 数目
这个公式的线程 cpu 时间是预估的程序单个线程在 cpu 上运行的时间(通常使用 loadrunner
测试大量运行次数求出平均值)

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

风铃峰顶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值