【java多线程】多线程基础总结:(程序进程线程)、(线程的创建)、(线程的方法)、(线程的优先级)、(守护线程)、(线程的生命周期)、(线程中断)、(线程的关闭)

并发与多线程

  • 几十年前,提起多核处理器和多核系统,通常价格不菲体积庞大,很多程序员都是顶礼膜拜,又有多少人能为他们编写程序呢?
  • 然而今天,多核已经成为不可阻挡的历史潮流,人们利用电子电路,在芯片嵌入更多处理单元,相对于昂贵复杂的多核系统,是提升系统并发性与性能简单、经济的途径。多核的影响是广泛且深入的,只有充分挖掘程序的并行性,才能让多核处理器物尽其用,才能在今后内核不断增加的日子里,得以保持升级。
  • 并发的优点:1、资源利用2、公平3、方便
  • 促进了进程的发展,也促进了线程的发展。线程允许程序控制流的多重分支同时存在于一个进程。它们共享进程范围内的资源,比如内存和文件句柄,但是每一个线程有自己的程序计数器,栈和本地变量。
  • 使用线程的优点:1)可以降低开发和维护的开销,并且能提高复杂应用的性能
    2)代码易读且容易维护
    并发和多线程之间的关系就如同目的和手段的关系
    java对线程内置的支持,通过提供语言和类库,以及一个规范的跨平台存储模型,简化了并发应用的开发,但同时要知道线程安全性的问题。下面来学习java多线程。

程序、进程与线程

程序是指令、数据及其组织形式的描述,进程是程序的实体。

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。

(简单来讲,进程就是运行起来的程序,编写的代码运行起来,或.exe文件运行起来就变成了程序)

线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。

多进程:Windows应用程序中消息有两种送出途径:直接和排队
Windodes或某些运行的应用程序可直接发布消息给窗口过程或者,消息可送到消息列象连续不断轮询消息队列的OS中当前执行的每个进程都 ,事件驱动程序不是由事件的顺序来控制,而是由事件的发生来控,而事件的发生是随机的、不确定的,这就允许程序的用户用各种合理的顺序来安排程序的流程。

多线程(multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。

进程和线程的区别和联系?

联系:1)一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程
2)线程是操作系统可识别的最小执行和调度单位。
资源分配给进程,同一进程的所有线程共享该进程的所有资源。
同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量)
扩展段(堆存储),但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放锁
有局部变量和临时变量
注:处理机包括中央处理器(CPU),主存储器,输入-输出接口,处理机外加接外围设备就构成完整的计算机系统。

区别:1)地址空间和其他资源(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其他进程内不可见。
线程间通信:同一个进程中的线程
2)通信:进程间通信IPC(管道,信号量,共享内存,消息队列,网络),线程间可以直接读
写进程数据段(如全局变量)来进程通信——需要进程同步和互斥手段的辅助,以保证数据的
一致性。
3)调度和切换:线程上下文切换比进程上下文切换快得多。
4)进程具有独立的空间地址,一个进程崩溃后,在保护模式下不会对其它进程产生影响。
保护模式下,程序地址为虚拟地址,然后由OS系统管理内存访问权限,这样每个进程只能访问分配给自己的物理线程只是一个进程的不同执行路径,线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉。

串行、并发和并行

串行:按照指令顺序,依次执行
并发:CPU快速切换给我们产生的同时执行的错觉
并行:真正的同时执行

并发和并行都是充分利用CPU的手段

线程的创建

创建线程的方式只有一种,就是new thread对象
但第一线程执行单元方式有三种

1、重写runnable接口下的run方法

如果要访问当前线程必须使用**Thread.currentThread()**方法
创建线程时需要先创建实现该接口类的对象,并将此对象作为Thread的参数来创建Thread对象

实现一个银行叫号程序在这里插入图片描述

题意:1)这四个叫号机可以同时出号
2)出号是1—50不重复
四个叫号机可以看成四个线程,在主线程中同时start()就可以实现
出号的顺序可以设成一个index、,用其++控制

注意:如果用的是继承Thread类,这里需将index设为静态变量(和对象无关),不然每new 一个线程都有一个自己的index。而这样会又引起线程安全问题。
而runnable接口很好的避免这一问题。

/**
 * 银行出号
 */
class BandTicketWindow implements Runnable {
    private int index = 1;//static成员和对象无关 共享变量,可能导致线程安全问题
    private int MAX = 50;

    @Override
    public void run() {
        while (index < MAX){
            System.out.println(Thread .currentThread() .getName() + "出号:" + index++);//多个线程对同一个index++
        }
    }
}

public class ThreadDome1 {
    public static void main(String[] args) {

        BandTicketWindow runable = new BandTicketWindow();
        Thread aThread=new Thread(runable );
        aThread.setName("一号机");
        aThread.start();

        Thread bThread=new Thread(runable );
        bThread.setName("二号机");
        bThread.start();

        Thread cThread=new Thread(runable );
        cThread.setName("三号机");
        cThread.start();

        Thread dThread=new Thread(runable );
        dThread.setName("四号机");
        dThread.start();
    }
}

2、重写Thread类下的run方法

因为已经继承了Thread类如果要访问当前线程,直接使用this即可获得当前线程

比如简单实现一个聊天的接受数据和发生数据
继承Thread类,重写run()方法,主线程中调用.start()方法,子线程处理接受数据,主线程处理发生数据
从控制台输出可以看到,接受数据和发送数据几乎是同时进行,这样就实现了发生和接受同时进行

class ReceiveThread extends Thread{   //子线程的定义
    @Override
    public void run() {
        for(int i = 0;i < 5000;i++){
            System.out.println("接收数据");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class ThreadTest {
    public static void main(String[] args) {  //主线程
        //处理发送数据
        ReceiveThread thread = new ReceiveThread();
        thread.start(); //启动子线程
//        for(int i = 0;i < 5000;i++){
//            System.out.println("接收数据");
//            try {
//                Thread.sleep(1000);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
//        }
        for(int i = 0;i < 5000;i++){
            System.out.println("发送数据");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

3、重写callable接口下call方法(有返回值)

如果要访问当前线程必须使用**Thread.currentThread()**方法
创建线程时需要先创建实现该接口类的对象,并将此对象作为FutureTask的参数,然后将futuretask对象作为thread的参数创建Thread对象
这个方法可以得到一个返回值,在合适的情况可以使用 task .get()

class CallableThread implements Callable <String >{

    @Override
    public String call() throws Exception {
        String s=Thread .currentThread() .getName() ;
        System.out.println(s);
        TimeUnit .SECONDS .sleep(7);
        return s;
    }
}
public class CallableTest {
    public static void main(String[] args) {
        CallableThread callable=new CallableThread() ;
        FutureTask <String >task =new FutureTask<>(callable );
        Thread thread =new Thread(task );
        thread .start() ;
        try {
            task .get() ;//运行在主线程中
            //get方法调用之后如果子线程运行结束,正常返回返回值
            //如果子线程未结束那么将会阻塞在这里直到子线程执行完返回返回值
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }
}

启动线程的方式

thread.start();
调用thread.run()方法就如同调用一个普通方法,并没有开启一个线程

这三种方式的联系和区别

继承类与实现接口的区别
a. 线程类集成字Thread则不能再继承其他类,而Runnable/Callable接口可以。
b. 线程类集成字Thread对于Runnable/callable来说,使用线程类内部的方法方便一些
c. 实现Runnable/Callable接口的线程类的多个线程可以更加方便的访问同一变量,而Thread类则需要内部类进行替代。
Callable 和 Runnable接口的区别
a. Callable规定的方法是call(),而Runnable规定的方法是run().
b. Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
c. call()方法可抛出异常,而run()方法是不能抛出异常的。
d. 运行Callable任务可拿到一个Future对象, *Future表示异步计算的结果。 提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。*通过Future对象可了解任务执行情况,可取消任务的执行,还可获取任务执行的结果。Callable是类似于Runnable的接口,实现Callable接口的类和实现Runnable的类都是可被其它线程执行的任务。

Thread类的基本方法

在这里插入图片描述

线程的优先级

每个线程有一个对应的优先级的数值
取值范围:(1–10)
默认的优先级是5

那么优先级高的会先执行吗?
A 5
B 6
通过控制台的输出,B并不是一定先于A执行

优先级高被CPU先执行概率大,并不是说优先级高的一定就先被执行,这里说的是概率问题。所以优先级通常不在项目开发中使用。

优先级的特点

(1)线程优先级的继承特性:也就是如果线程A启动线程B,那么线程A和B的优先级是一样的;
(2)线程优先级的规则性:即线程会优先级的大小顺序执行,但是不一定是优先级较大的先执行完,因为线程的优先级还有下面第三个特性:
(3)线程优先级的
不确定性和随机性
:线程优先级的随机特性优先级越高,大部分情况下在多个线程的情况下优先级高的先完成所有的任务。但是优先级并不是衡量那个线程运行结果顺序的标准,因为cpu只是尽量将执行资源交给优先级高的线程。优先级高的线程不一定每一次都先执行完run方法中的任务,也就是说线程优先级与打印顺序无关,他们的关系具有不确定性和随机性。

守护线程

java中线程分两种**:“用户线程”、“守护线程”**

守护线程(服务线程)是一个特殊的线程。 在后台执行的线程,当所有非守护线程结束之后,守护线程会自动结束。

默认的命名方式:Thread-编号

为什么要设置守护线程?

比如搭建一个戏台,线程就如舞者,如果戏台拆掉,舞者自然就随着戏台的拆除而结束,不能继续起舞。 舞者特点和守护线程的特点一样,所以此时舞者该设置为守护线程。

thread.setDaemon(true);

就将此线程设置为守护线程,需放到thread.start()前面

用户线程和守护线程的区别

1、 主线程结束后用户线程还会继续运行,JVM存活;主线程结束后守护线程和JVM的状态又下面第2条确定。

2、如果没有用户线程,都是守护线程,那么JVM结束(随之而来的是所有的一切烟消云散,包括所有的守护线程)。

使用实例

jvm的java垃圾回收机制是典型的守护线程的例子,
当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。

游戏中的装备和金币等信息也是守护线程,实时监控

线程的生命周期

线程的生命周期,顾名思义就是线程由生到死的过程。

线程的状态

在这里插入图片描述
由图得:
线程一共有六个状态
New Runnable blocked waiting timed_waiting terminated

1.New(初始)

**当我们用关键字new创建一个thread对象时,**此时它并不是出于执行状态,因为没有调用start方法启动该线程,那么线程的状态为new状态准确地说,它只是Thread对象的状态,因为没有start之前,该线程根部就不存在,与你用关键字new创建一个普通的java对象没有什么区别new 状态通过start方法进入runnable状态。

2. Runnable(运行)

Runnable状态:分成了两种状态可执行(就绪态)和运行态。

1)就绪态 已经调用了start方法,但是线程还没有被cpu调度执行。只是具有了运行的资格。

2)线程对象进入就可执行(就绪态)态必须调用start方法,那么此时才是真正的在jvm进程中创建一个线程,线程一经启动就可以立即找得到执行?
线程的运行如否和进程一样都要听命于cpu的调度,那么我们把中间状态成为可执行状态(就绪态),它是具备执行的资格,但是并没有真正的执行起来,而是在等待cpu的调度由于存在运行状态,所以不会直接进入blocked状态、waiting、timed waiting和terminated状态,即使是在现层的执行逻辑中调用wait、sleep或者其他block的io操作等,也必须获得cpu的调度执行权才可以严格来讲,可执行(就绪态)的线程只能意外终止或者进入运行状态;

3. Blocked(阻塞)

线程阻塞状态。
进行某个阻塞的io操作,比如因网络数据的读写而进入的blocked状态,获取某个锁资源,从而加入到该锁的阻塞队列中进入blocked状态。

4. Waiting(等待)

一个线程在等待执行一个(唤醒)动作时,该线程进入Waiting状态。
进入这个状态后不能自动唤醒,必须等待另一个线程发出特定的指令才能够被唤醒。

将线程设为waiting态的方法有
Condition.await()
Object.wait()
Object.join()
LockSupport.part()

CPU在运行程序时,主线程main和子线程都会执行,如果主线程输出的结果和子线程的执行有关,则可以将主线程设为等待状态,等子线程执行完再执行主线程

使用

CountDownLatch 的使用最为常用
例如:要实现一个查询航班的系统,所有的航空公司航班都查完再输出结果
a:用join方法,callable实现

class MyCallThread implements Callable<String >  {


    @Override
    public String  call() throws Exception {
        String s=Thread .currentThread() .getName() ;
        System.out.println("当前信息:"+ s);
        return s;
    }
}
public class TestDome {
    public static void main(String[] args) {

        MyCallThread callThread1 =new MyCallThread( ) ;
        FutureTask <String >task1 =new FutureTask<>(callThread1 );
        Thread thread1=new Thread(task1 );

        MyCallThread callThread2 =new MyCallThread() ;
        FutureTask <String >task2 =new FutureTask<>(callThread1 );
        Thread thread2=new Thread(task2 );

        MyCallThread callThread3 =new MyCallThread() ;
        FutureTask <String >task3 =new FutureTask<>(callThread1 );
        Thread thread3=new Thread(task3 );
        thread1 .setName("南方航空");
        thread2 .setName("西方航空");
        thread3 .setName("海南航空");
        
         thread1 .start() ;
         thread2 .start() ;
         thread3 .start() ;

        List <String >  mylist=new ArrayList<>() ;
        try {
            mylist .add(task1 .get() );
            mylist .add(task2 .get() );
            mylist .add(task3 .get() );
        } catch (Exception e) {
            e.printStackTrace();
        }

        try {
            thread1 .join() ;
            thread2 .join() ;
            thread3 .join() ;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

       for(int i=0;i<mylist .size() ;i++){
           System.out.println(mylist .get(i) );
       }
    }
}

主线程的sout输出list需要在所有线程运行完毕

b:用runnable,await方法

class AirPlaneSearch implements Runnable  {
    List<String> list;
    CountDownLatch latch ;

    public AirPlaneSearch(List<String> list, CountDownLatch latch) {
        this.list = list;
        this.latch = latch;
    }

    @Override
    public void run() {   //查询各个航空公司的信息
        System.out.println("开始从" + Thread.currentThread().getName() + "查询航班信息");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (list) {
            list.add(Thread.currentThread().getName() + "8:00");
        }
        latch.countDown(); //减一   count 执行了一次 减一
    }
}

public class AirTest {
    public static void main(String[] args) throws InterruptedException {
        List<String> list = new LinkedList<>();
        CountDownLatch latch = new CountDownLatch(3); //有几个子线程就设置位几
        AirPlaneSearch runnable = new AirPlaneSearch(list,latch);
        Thread t1 = new Thread(runnable, "南方航空");
        Thread t2 = new Thread(runnable, "东方航空");
        Thread t3 = new Thread(runnable, "海南航空");
        t1.start();
        t2.start();
        t3.start();
        try {
            latch.await();    //等待latch中的count被减为 0 什么时候减为 0 后面的代码才开始执行
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Iterator<String> iterator = list.iterator();
        System.out.println("--------------------------------------------");
        System.out.println("所查询的航班信息如下: ");
        while (iterator.hasNext()){
            System.out.println(iterator.next());
        }
    }
}

倒序输出线程
a:在主线程中.join()
class MyRunnable implements Runnable {
CountDownLatch count ;
public MyRunnable ( CountDownLatch count){
this .count =count ;
}
@Override
public void run() {
for(int i=0;i<10;i++) {
System.out.println(Thread.currentThread().getName());
}
count .countDown() ;
}

}
public class TestDome2 {
public static void main(String[] args)throws InterruptedException {
CountDownLatch count =new CountDownLatch(1);
MyRunnable myRunnable =new MyRunnable(count ) ;

    Thread thread =new Thread(myRunnable ,"线程1开始执行");
    Thread thread1 =new Thread(myRunnable ,"线程2开始执行");
    Thread thread2 =new Thread(myRunnable ,"线程3开始执行");

// thread2 .start() ;
// thread2 .join() ;
// thread1 .start() ;
// thread1 .join() ;
// thread .start() ;
thread2 .start() ;
count .await() ;
thread1 .start() ;
count .await() ;
thread .start() ;

}

}
b:在子线程中join

class Tthread extends Thread {
    private Thread t;

    public Tthread() {

    }

    public Tthread(Thread t) {
        this.t = t;
    }

    @Override
    public void run() {
        //t3.join
        //t2.join
        try {
            if (t != null) {
                t.join();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(this.getName() + "已完成");
    }
}

public class test1 {
    public static void main(String[] args) {
        Tthread t3 = new Tthread();
        Tthread t2 = new Tthread(t3);
        Tthread t1 = new Tthread(t2);
        t1.setName("t1");
        t2.setName("t2");
        t3.setName("t3");
        t3.start();
        t2.start();
        t1.start();
    }
}

5. Time_waiting(超时等待)

和上面的等待类似,只是这个状态的线程不会一直等下去,会有一个超时时间。

最常用的Thread.sleep(long);
Object.wait(long);
Thread.join(long);
LockSupport.parkNanos();
LockSupport.parkUntil();

6. Terminated(终止)

terminated是一个线程的最终状态,在该状态中线程将不会切换到其他任何状态,*
线程进入terminated状态,意味着该线程的整个生命周期结束了。
线程运行正常结束,结束生命周期;
线程运行出错意外结束, JVM crash导致所有的线程都结束

线程中断

java中断并非直接中断线程的运行,中断只是一种协作机制,每个线程底层都保留一个中断标志符,程序可以调用中断方法,但是是否真正的中断还是依赖于应用程序本身的处理,中断有对应2种情况,在线程阻塞时,对于某些特别的阻塞,例如Thread方法的sleep,Object的wait方法,以及一些nio的阻塞,阻塞队列的take方法等,如果在阻塞的状态下调用interrupt()方法,会导致抛出InterruptedException异常,并重置其中断状态,即为false。而在另一些常规的IO阻塞和同步阻塞,设置中断,都只会改变其中断状态,程序可以处理,也可以不处理。

中断的方式

thread.interrupt()
设置线程中断的唯一方法
thread.interrupted()
静态方法,获取当前线程是否中断的方法,获取之后中断状态将会被重置,例如调用interrupt()方法,再调用interrupted()方法会返回true,第二次调用会返回false,因为中断的状态被重置了。
thread.isinterruppted()
返回线程的中断状态,中断不会被此方法改变。

实例

在之前的聊天系统上,先只接受数据20s后,再让接受和发送同时进行

class ReceiveThread extends Thread {//子线程的定义
    Thread thread;
    private int i = 0;

    public ReceiveThread(String str, Thread thread) {
        super(str);
        this.thread = thread;
    }

    @Override
    public void run() {  //线程的执行单元

        while (true){
            i++;
            System.out.println(this.getName());
            if(i == 20){
                thread.interrupt();  //中断了主线程的等待状态
            }
            try {
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class Test {
    public static void main(String[] args) {  //主线程
        //处理发送数据
        ReceiveThread thread = new ReceiveThread("接收数据",Thread.currentThread());
        thread.start(); //启动子线程
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        while (true){
            System.out.println("发送数据");
            try {
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

如何关闭一个线程

stop()方法分析

线程启动完毕后,在运行时可能需要终止,Java提供的终止方法只有一个stop,但是我不建议使用这个方法,因为它有以下三个问题:(1)stop方法是过时的从Java编码规则来说,已经过时的方法不建议采用。(2)stop方法会导致代码逻辑不完整stop方法是一种“恶意”的中断,一旦执行stop方法,即终止当前正在运行的线程,不管线程逻辑是否完整,这是非常危险的。(3)stop方法会破坏原子逻辑多线程为了解决共享资源抢占的问题,使用了锁概念,避免资源不同步,但是正因此原因,stop方法却会带来更大的麻烦:它会丢弃所有的锁,导致原子逻辑受损。

Java没有安全的抢占方法来停止线程,只有协作式的机制,使代码遵循一种协商好的协议来提前结束线程。
其中一种协作机制是通过设置标志(已请求取消),任务将定期查看该标志,如果设置了该标志,则任务提前结束。

方法一:

使用interrupt,原理还是根据中断位判断
但是线程中还有其他方法可以改变中断位,如果调用了就改变了中断位,从而使得结果收到影响
改变中断位的方法:
Object的wait/wait(long)/wait(long,int)方法

Thread的sleep(int)/sleep(long,int)

Thread的join/join(long)/join(long,int)方法

class AThread implements Runnable{

    @Override
    public void run() {

        while (true){
            if(Thread .currentThread() .isInterrupted() ){
                break;
            }
            System.out.println("接收数据");
        }
    }
}
public class CloseThread {
    public static void main(String[] args) {
        AThread runnable = new AThread();
        Thread  t = new Thread(runnable);
        t.start();

        try {
            TimeUnit.SECONDS.sleep(50);

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.interrupt() ;
    }
}

方法二

常用的方法是使用volatile关键字修饰标志

class AThread implements Runnable {

    private static volatile boolean flag = false;

    public static void closeThread(){
        flag = true;
    }

    @Override
    public void run() {
        while (true) {
            if (flag) {
                break;
            }
            System.out.print("接收数据");
        }
    }
}

public class CloseThread {
    public static void main(String[] args) {
        AThread runnable = new AThread();
        Thread t = new Thread(runnable);
        t.start();
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        AThread.closeThread();
    }
}

但是这样的方法会存在线程安全问题,具体下次博客分析~

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值