Java多线程之一:Java多线程技能

什么是多线程

线程是一个单独程序流程。多线程是指一个程序可以同时运行多个任务,每个任务由一个单独的线程来完成。也就是说,多个线程可以同时在一个程序中运行,并且每一个线程完成不同的任务。程序可以通过控制线程来控制程序的运行,例如线程的等待、休眠、唤起线程等。

什么是进程

进程是一个正在执行的程序 ,比如QQ,迅雷等 一个进程的运行会向CPU申请在内存中开辟一个内存块。 

他是向CPU申请资源的,进程之间数据相互独立,一个进程至少有一个线程。

什么是线程

       传统的程序设计语言同一时刻只能执行单任务操作,效率非常低,如果网络程序在接收数据时发生阻塞,只能等到程序接收数据之后才能继续运行。随着 Internet 的飞速发展,这种单任务运行的状况越来越不被接受。如果网络接收数据阻塞,后台服务程序就会一直处于等待状态而不能继续任何操作。 这种阻塞情况经常发生, 这时的 CPU资源完全处于闲置状态。多线程实现后台服务程序可以同时处理多个任务,并不发生阻塞现象。多线程是 Java 语言的一个很重要的特征。 多线程程序设计最大的特点就是能够提高程序执行效率和处理速度。Java 程序可同时并行运行多个相对独立的线程。例如创建一个线程来接收数据,另一个线程发送数据,既使发送线程在接收数据时被阻塞,接受数据线程仍然可以运行。 线程(Thread)是控制线程(Thread of Control)的缩写,它是具有一定顺序的指令序列(即所编写的程序代码)、存放方法中定义局部变量的栈和一些共享数据。线程是相互独立的,每个方法的局部变量和其他线程的局部变量是分开的,因此,任何线程都不能访问除自身之外的其他线程的局部变量。如果两个线程同时访问同一个方法,那每个线程将各自得到此方法的一个拷贝。Java 提供的多线程机制使一个程序可同时执行多个任务。线程有时也被称为小进程,它是从一个大进程里分离出来的小的独立的线程。由于实现了多线程技术,Java 显得更健壮。多线程带来的好处是更好的交互性能和实时控制性能。多线程是强大而灵巧的编程工具,但要用好它却不是件容易的事。在多线程编程中,每个线程都通过代码实现线程的行为,并将数据供给代码操作。编码和数据有时是相当独立的,可分别向线程提供。多个线程可以同时处理同一代码和同一数据,不同的线程也可以处理各自不同的编码和数据。  

多线程与多进程的区别

多进程拥有自己的一套数据变量,而多线程是共享数据,而共享数据也会带来一系列的安全问题。

创建线程方法

Java程序都是声明一个公共类,并在类内实现一个 main 方法。事实上,这些程序就是一个单线程程序。当它执行完main 方法的程序后,线程正好退出,程序同时结束运行。

public class OnlyThread {  
    public static void main(String args[]) {  
        run(); // 调用静态run()方法  
    }  
  
    /** 
     * 实现run()方法 
     */  
    public static void run() {  
        // 循环计算输出的*数目  
        for (int count = 1, row = 1; row < 10; row++, count++) {  
            for (int i = 0; i < count; i++) { // 循环输出指定的count数目的*  
                System.out.print('*');   
            }  
            System.out.println();  
        }  
    }  
}  
只是建立了一个单一线程并执行的普通小程序,并没有涉及到多线程的概念。

      在 Java程序中,有两种方法创建线程:

         一是对 Thread 类进行派生并覆盖 run方法;

         二是通过实现 runnable接口创建。 

      当你打算多重继承时,优先选择实现Runnable。

Thread 创建线程

 在程序中创建新的线程的方法之一是继承 Thread 类, 并通过 Thread子类声明线程对象。继承Thread 类并覆盖 Thread类的 run 方法完成线程类的声明, 通过new创建派生线程类的线程对象。run 中的代码实现了线程的行为。 

      java.lang.Thread 类是一个通用的线程类,由于默认情况下 run 方法是空的,直接通过 Thread类实例化的线程对象不能完成任何事,所以可以通过派生 Thread 类,并用具体程序代码覆盖Thread 类中的 run 方法,实现具有各种不同功能的线程类。

      1) Thread 创建线程步骤: 

     (1)创建一个新的线程类,继承 Thread 类并覆盖 Thread 类的 run()方法。 
class ThreadType extends Thread{   
     public void run(){   
         ……   
     }   
}   
(2)创建一个线程类的对象,创建方法与一般对象的创建相同,使用关键字new完成。  
ThreadType  tt = new ThreadType();   

 (3)启动新线程对象,调用 start()方法。 

tt.start();   

(4)线程自己调用 run()方法。 

void run();   

2) Thread创建一个线程

     下面是通过Thread创建线程的例子:产生一个新的线程 

class ThreadDemo1 extends Thread {  
    ThreadDemo1() {  
    }  
  
    // 声明ThreadDemo1带参数的构造方法  
    ThreadDemo1(String szName) {  
        super(szName);  
    }  
  
    // 重载run函数  
    public void run() {  
        for (int count = 1, row = 1; row < 10; row++, count++) {  
            for (int i = 0; i < count; i++) {// 循环输出指定的count数目的*  
                System.out.print('*');  
            }  
            System.out.println();  
        }  
    }  
  
    public static void main(String argv[]) {  
        ThreadDemo1 td = new ThreadDemo1(); // 创建,并初始化ThreadDemo1类型对象td  
        td.start(); // 调用start()方法执行一个新的线程  
    }  
}  
OnlyThread.java程序与程序ThreadDemo1 .java 表面上看运行结果相同,但是仔细对照会发现,程序 OnlyThread.java 中对 run方法的调用在程序 ThreadDemo1.java 中变成了对 start 方法的调用,并且程序 ThreadDemo1.java 确派生 Thread类,创建新的线程类。 

3) Thread创建多个线程

//文件:程序10.3 ThreadDemo2.java   描述:产生三个新的线程 

public class ThreadDemo2 extends Thread {  
    // 声明无参数,空构造方法  
    ThreadDemo2() {  
    }  
  
    // 声明带有字符串参数的构造方法  
    ThreadDemo2(String szName) {  
        super(szName); // 调用父类的构造方法  
    }  
  
    // 重载run函数  
    public void run() {  
        for (int count = 1, row = 1; row < 10; row++, count++) {  
            for (int i = 0; i < count; i++) {// 循环输出指定的count数目的*  
                System.out.print('*');  
            }  
            System.out.println();  
        }  
    }  
  
    public static void main(String argv[]) {  
        ThreadDemo2 td1 = new ThreadDemo2(); // 创建,并初始化ThreadDemo2类型对象td1  
        ThreadDemo2 td2 = new ThreadDemo2(); // 创建,并初始化ThreadDemo2类型对象td2  
        ThreadDemo2 td3 = new ThreadDemo2(); // 创建,并初始化ThreadDemo2类型对象td3  
        td1.start(); // 启动线程td1  
        td2.start(); // 启动线程td2  
        td3.start(); // 启动线程td3  
    }  
}  


注意:Java线程并不能按调用顺序执行,而是并行执行的单独代码。如果要想得到完整的直角三角形,需要在执行一个线程之前,判断程序前面的线程是否终止,如果已经终止,再来调用该线程。

 

Runnable 接口创建线程 

 通过实现 Runnable 接口的方法是创建线程类的第二种方法。利用实现 Runnable 接口来创建线程的方法可以解决 Java 语言不支持的多重继承问题。Runnable 接口提供了 run()方法的原型,因此创建新的线程类时,只要实现此接口,即只要特定的程序代码实现Runnable接口中的 run()方法,就可完成新线程类的运行。

      扩展Thread类创建线程的方式,适合编写简单的应用程序代码,而实现Runnable接口创建线程,能够避免Java单继承的局限,适合同一代码的多线程处理同一资源的情况,代码具有良好的一致性,是更符合面向对象思想的设计方式。

     1) Runnable 创建线程步骤 

   (1)创建一个实现 Runnable 接口的类,并且在这个类中重写 run 方法。 

class ThreadType implements Runnable{   
     public void run(){   
         ……   
     }   
}   

(2)使用关键字 new新建一个 ThreadType 的实例。 

Runnable rb = new ThreadType ();   

   (3)通过 Runnable 的实例创建一个线程对象,在创建线程对象时,调用的构造函数是new Thread(ThreadType),它用 ThreadType 中实现的 run()方法作为新线程对象的 run()方法。 

Thread td = new Thread(rb);   

(4)通过调用 ThreadType 对象的 start()方法启动线程运行。 

td.start();   

 2) Runnable 创建线程

class ThreadDemo3 implements Runnable {  
    // 重载run函数  
    public void run() {  
        for (int count = 1, row = 1; row < 10; row++, count++){ // 循环计算输出的*数目  
            for (int i = 0; i < count; i++){ // 循环输出指定的count数目的*  
                System.out.print('*');   
            }  
            System.out.println();   
        }  
    }  
  
    public static void main(String argv[]) {  
        Runnable rb = new ThreadDemo3(); // 创建,并初始化ThreadDemo3对象rb  
        Thread td = new Thread(rb); // 通过Thread创建线程  
        td.start(); // 启动线程td  
    }  
}  


  • 一些基本API:isAlive(),sleep(),getId(),yield()等。

    • isAlive()测试线程是否处于活动状态

    • sleep()让“正在执行的线程”休眠

    • getId()取得线程唯一标识

    • yield()放弃当前的 CPU 资源

JDK1.0 定义了stop和suspend方法,stop用来直接终止线程,suspend会阻塞线程直到另一个线程调用resume.
       stop和suspend都有一些共同的点:都试图专横的控制一个给定了的线程的行为.

 从JDK1.2  开始,这两个方法都被弃用了.stop天生就不安全,因为可能产生数据不同步等问题


线程的状态

 线程的整个周期由线程创建、可运行状态、不可运行状态和退出等部分组成,这些状态之间的转化是通过线程提供的一些方法完成的。

1.线程周期

      一个线程有4 种状态,任何一个线程都处于这4种状态中的一种状态: 
      1) 创建(new)状态:调用 new方法产生一个线程对象后、调用 start 方法前所处的状态。线程对象虽然已经创建,但还没有调用 start 方法启动,因此无法执行。当线程处于创建状态时,线程对象可以调用 start 方法进入启动状态,也可以调用 stop 方法进入停止状态。 
     2)可运行(runnable)状态:当线程对象执行 start()方法后,线程就转到可运行状态。进入此状态只是说明线程对象具有了可以运行的条件,但线程并不一定处于运行状态。因为在单处理器系统中运行多线程程序时,一个时间点只有一个线程运行,系统通过调度机制实现宏观意义上的运行线程共享处理器。 因此一个线程是否在运行,除了线程必须处于 Runnable 状态之外,还取决于优先级和调度。 
    3)不可运行(non Runnable)状态:线程处于不可运行状态是由于线程被挂起或者发生阻塞,例如对一个线程调用 wait()函数后,它就可能进入阻塞状态;调用线程的notify或notifyAll 方法后它才能再次回到可执行状态。 
     4)退出(done)状态:一个线程可以从任何一个状态中调用 stop 方法进入退出状态。线程一旦进入退出状态就不存在了,不能再返回到其他的状态。除此之外,如果线程执行完 run方法,也会自动进入退出状态。 
     创建状态、可运行状态、不可运行状态、退出状态之间的转换关系如图 所示。 



通过 new第一次创建线程时,线程位于创建状态,这时不能运行线程,只能等待进一步的方法调用改变其状态。然后,线程通过调用 start方法启动线程,并进入可执行状态,或者调用方法 stop进入退出状态。当程序位于退出状态时,线程已经结束执行,这是线程的最后一个状态,并且不能转化到其他状态。当程序的所有线程位于退出状态时,程序会强行终止。当线程位于可执行状态时,在一个特定的时间点上,每一个系统处理器只能运行一个线程。 此时如果线程被挂起,执行就会被中断或者进入休眠状态,那么线程将进入不可执行状态,并且不可执行状态可以通过 resume、notify等方法返回到可执行状态。表10-1列举了线程状态转换的函数。 


  线程状态转换函数: 
方法                        描述                                                          有效状态             目的状态 
start()                     开始执行一个线程                                     New                     Runnable 
stop()                     结束执行一个线程                                     New或Runnable    Done 
sleep(long)           暂停一段时间,这个时间为给定的毫秒   Runnable             NonRunnable 
sleep(long,int)     暂停片刻,可以精确到纳秒                     Runnable             NonRunnable 
suspend()             挂起执行                                                      Runnable             NonRunnable 
resume()               恢复执行                                                       NonRunnable       Runnable 
yield()                    明确放弃执行                                               Runnable             Runnable 
wait()                     进入阻塞状态                                               Runnable             NonRunnable 
notify()                   阻塞状态解除                                               NonRunnable       Runnable 
注意:stop()、suspend()和 resume()方法现在已经不提倡使用,这些方法在虚拟机中可能引起“死锁”现象。suspend()和 resume()方法的替代方法是 wait()和 sleep()。线程的退出通常采用自然终止的方法,建议不要人工调用 stop()方法。 


2  线程的创建和启动 

      Java 是面向对象的程序设计语言,设计的重点就是类的设计与实现。Java 利用线程类Thread 来创建线程,线程的创建与普通类对象的创建操作相同。Java通过线程类的构造方法创建一个线程,并通过调用 start 方法启动该线程。 
       实际上,启动线程的目的就是为了执行它的 run()方法,而 Thread 类中默认的 run()方法没有任何可操作代码,所以用 Thread类创建的线程不能完成任何任务。为了让创建的线程完成特定的任务,必须重新定义 run()方法。在第一节中已经讲述过,Java 通常有两种重新定义run()方法的方式:

      1)派生线程类 Thread 的子类,并在子类中重写 run()方法:Thread 子类的实例对象是一个线程对象,并且该线程有专门定制的线程 run()方法,启动线程后就执行子类中重写的 run()方法。 
‰      2)实现 Runnable 接口并重新定义 run()方法:先定义一个实现 Runnable()接口的类,在该类中定义 run()方法,然后创建新的线程类对象,并以该对象作为 Thread 类构造方法的参数创建一个线程。 
      
      注意:调用线程的 run()方法是通过启动线程的start()方法来实现的。 因为线程在调用start()方法之后,系统会自动调用 run()方法。与一般方法调用不同的地方在于一般方法调用另外一个方法后,必须等被调用的方法执行完毕才能返回,而线程的 start()方法被调用之后,系统会得知线程准备完毕并且可以执行run()方法,start()方法就返回了,start()方法不会等待run()方法执行完毕。  


3 线程状态转换 

1.线程进入可执行状态 
    当以下几种情况发生时,线程进入可执行状态。 
(1)其他线程调用notify()或者 notifyAll()方法,唤起处于不可执行状态的线程。 
      public final void notify() 
      public final void notifyAll() 
     notify 仅仅唤醒一个线程并允许它获得锁,notifyAll 唤醒所有等待这个对象的线程,并允许它们获得锁。
(2)线程调用 sleep(millis)方法,millis毫秒之后线程会进入可执行状态。 
         static void sleep(long millis) throws InterruptedException 在 millis 毫秒数内让当前正在执行的线程进入休眠状态,等到时间过后,该线程会自动苏醒并继续执行。sleep方法的精确度受到系统计数器的影响。 
        static void sleep(long millis, int nanos) throws InterruptedException 在毫秒数(millis)加纳秒数(nanos)内让当前正在执行的线程进入休眠状态,此操作的精确度也受到系统计数器的影响。 

(3)线程对I/O操作的完成。 

2 线程进入不可执行状态 


    当以下几种情况发生时,线程进入不可执行状态。 
(1)线程自动调用 wait()方法,等待某种条件的发生。 
      public final void wait() throws InterruptedException 
     当其他线程调用 notify()方法或 notifyAl()方法后,处于等待状态的线程获得锁之后才会被唤醒,然后该线程一直等待重新获得对象锁才继续运行。 
(2)线程调用 sleep()方法进入不可执行状态,在一定时间后会进入可执行状态。 
(3)线程等待 I/O操作的完成。 

   线程阻塞的例子:

public class  ThreadSleep   
{   
 public static void main(String[ ] args)    
 {   
  SubThread st = new SubThread("SubThread");  //创建,并初始化SubThread 对象st   
  st.start();         //启动线程st   
 }   
}   
   
class SubThread extends Thread{   
 SubThread(){}        //声明,实现SubThread无参数构造方法   
 //声明,实现SubThread带字符串参数构造方法   
 SubThread(String Name)   
 {   
  super(Name);        //调用父类的构造方法   
 }   
 //重载run函数   
 public void run()   
 {   
  for (int count = 1,row = 1; row < 10; row++,count++) //循环计算输出的*数目   
  {   
   for (int i = 0; i < count; i++)      //循环输出指定的count数目的*   
   {   
    System.out.print('*');     //输出*   
   }   
   try         //try-catch块,用于捕获异常   
   {     Thread.sleep(1000);     //线程休眠1秒钟   
    System.out.print("\t wait........");   
   }   
   catch (InterruptedException e)    //捕获异常InterruptedException   
   {   
    e.printStackTrace();     //异常抛出信息   
   }   
   System.out.println();      //输出换行符   
  }   
 }   
}   
程序ThreadSleep 中,每输出一行*就要休息1 秒钟。当执行 sleep()语句后, 线程进入不可执行状态等待1 秒钟之后,线程 st 会自动苏醒并继续执行。由于 sleep方法抛出 InterruptedException异常, 所以在调用时必须捕获异常。 

4 等待线程结束 

isAlive()方法用来判断一个线程是否存活。当线程处于可执行状态或不可执行状态时,isAlive()方法返回 true; 当线程处于创建状态或退出状态时, 则返回 false。 也就是说, isAlive()方法如果返回 true,并不能判断线程是处于可运行状态还是不可运行状态。isAlive()方法的原型如下所示。 
      public final boolean isAlive() 

      该方法用于测试线程是否处于活动状态。活动状态是指线程已经启动(调用 start方法)且尚未退出所处的状态,包括可运行状态和不可运行状态。可以通过该方法解决程序中的问题,先判断第一个线程是否已经终止,如果终止再来调用第二个线程。这里提供两种方法:

      第一种方法是不断查询第一个线程是否已经终止,如果没有,则让主线程睡眠一直到它终止即“while/isAlive/sleep”,格式如下。 

线程1.start();   
while(线程 1.isAlive()) {   
 Thread.sleep(休眠时间);   
}   
线程2.start();   
 第二种是利用 join()方法。 
    1)public final void join(long millis) throws InterruptedException 等待该线程终止的时间最长为毫秒(millis),超时为0 意味着要一直等下去。 
    2) public final void join(long millis,int nanos) throws InterruptedException 等待该线程终止的时间最长为毫秒(millis)加纳秒(nanos)。  
    3)public final void join() throws InterruptedException 等待该线程终止。 
    等待线程结束并执行另外一个线程的例子。等待一个线程的结束的两种方法  :
package Test;  
  
class WaitThreadStop extends Thread {  
    // 声明,并实现WaitThreadStop无参数构造方法  
    WaitThreadStop() {  
    }  
  
    // 声明,并实现带有一个字符串参数的构造方法  
    WaitThreadStop(String szName) {  
        super(szName); // 调用父类的构造方法  
    }  
  
    // 重载run函数  
    public void run() {  
        for (int count = 1, row = 1; row < 10; row++, count++) {  
            for (int i = 0; i < count; i++) {  
                System.out.print('*'); // 输出*  
            }  
            System.out.println(); // 输出换行符  
        }  
    }  
}  
  
public class WaitThreadStopMain {  
    public static void main(String argv[ ]){   
      WaitThreadStopMain test = new WaitThreadStopMain();    //创建,初始化WaitThreadStopMain对象test   
      test.Method1();  //调用Method1方法   
      //test.Method2();   
     }  
    // 第一种方法:while/isAlive/sleep  
    public void Method1() {  
        WaitThreadStop th1 = new WaitThreadStop(); // 创建,并初始化WaitThreadStop对象th1  
        WaitThreadStop th2 = new WaitThreadStop(); // 创建,并初始化WaitThreadStop对象th2  
        // 执行第一个线程  
        th1.start();  
        // 查询第一个线程的状态  
        while (th1.isAlive()) {  
            try {  
                Thread.sleep(100); // 休眠100毫秒  
            } catch (InterruptedException e) {  
                e.printStackTrace(); // 异常信息输出  
            }  
        }  
        // 当第一个线程终止后,运行第二个线程  
        th2.start(); // 启动线程th2  
    }  
  
    // 第二种方法,使用join方法实现等待其他线程结束  
    public void Method2() {  
        WaitThreadStop th1 = new WaitThreadStop(); // 创建, 并初始化WaitThreadStop对象th1  
        WaitThreadStop th2 = new WaitThreadStop(); // 创建,并初始化WaitThreadStop对象th2  
        // 执行第一个线程  
        th1.start();  
        try {  
            th1.join(); // th1调用join 方法  
        } catch (InterruptedException e) {  
            e.printStackTrace(); // 异常信息输出  
        }  
        // 执行第二个线程  
        th2.start();  
    }  
}  

线程调度

     多线程应用程序的每一个线程的重要性和优先级可能不同,例如有多个线程都在等待获得CPU的时间片, 那么优先级高的线程就能抢占CPU并得以执行; 当多个线程交替抢占CPU时,优先级高的线程占用的时间应该多。因此,高优先级的线程执行的效率会高些,执行速度也会快些。 

       在 Java 中,CPU的使用通常是抢占式调度模式不需要时间片分配进程。抢占式调度模式是指许多线程同时处于可运行状态,但只有一个线程正在运行。当线程一直运行直到结束,或者进入不可运行状态,或者具有更高优先级的线程变为可运行状态,它将会让出 CPU。线程与优先级相关的方法如下:

      public final void setPriority(int newPriority) 设置线程的优先级为 newPriority :          

      newPriority 的值必须在 MIN_PRIORITY 到MAX_PRIORITY范围内,通常它们的值分别是1和10。目前Windows系统只支持3个级别的优

先级, 它们分别是Thread.MAX_PRIORITY、 Thread.MIN_PRIORITY和Thread.NORM_PRIORITY。  

      public final int getPriority() 获得当前线程的优先级。

     线程优先级的例子:

class  InheritThread extends Thread {   
    //自定义线程的run()方法   
    public void run(){   
         System.out.println("InheritThread is running…"); //输出字符串信息   
         for(int i=0;i<10;i++){   
              System.out.println(" InheritThread: i="+i);  //输出信息   
              try{   
                  Thread.sleep((int)Math.random()*1000); //线程休眠   
             }   
             catch(InterruptedException e)     //捕获异常   
             {}   
        }   
    }   
}   
通过Runnable接口创建的另外一个线程 :
class RunnableThread implements Runnable {  
    // 自定义线程的run()方法  
    public void run() {  
        System.out.println("RunnableThread is running…"); // 输出字符串信息  
        for (int i = 0; i < 10; i++) {  
            System.out.println("RunnableThread : i=" + i); // 输出i  
            try {  
                Thread.sleep((int) Math.random() * 1000); // 线程休眠  
            } catch (InterruptedException e) { // 捕获异常  
            }  
        }  
    }  
}  
  
public class ThreadPriority {  
    public static void main(String args[]) {  
        // 用Thread类的子类创建线程  
        InheritThread itd = new InheritThread();  
        // 用Runnable接口类的对象创建线程  
        Thread rtd = new Thread(new RunnableThread());  
        itd.setPriority(5); // 设置myThread1的优先级5  
        rtd.setPriority(5); // 设置myThread2的优先级5  
        itd.start(); // 启动线程itd  
        rtd.start(); // 启动线程rtd  
    }  
}  

在程序ThreadPriority.java中,线程 rtd 和 itd 具有相同的优先级,所以它们交互占用 CPU,宏观上处于并行运行状态。结果如图3. 

重新设定优先级: 

itd.setPriority(1);  //设置myThread1的优先级1 
rtd.setPriority(10); //设置myThread2的优先级10 
运行程序结果如图4所示。 

 
    图3  相同优先级                                图4  不同的优先级

       从运行结构可以看出程序ThreadPriority.java修改后,由于设置了线程itd和 rtd 的优先级,并且 rtd的优先级较高,基本上是 rtd都优先抢占 CPU资源。 
          


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值