控制线程

[size=medium]1、线程睡眠:sleep[/size]

如果我们需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread类的[b]静态[/b]sleep方法,sleep方法有两种重载的形式:

● public static void sleep(long millis) throws InterruptedException
让当前正在执行的线程暂停millis毫秒,并进入阻塞状态,该方法受到系统计时器和线程调度器的精度和准确性的影响。

● public static void sleep(long millis,int nanos) throws InterruptedException
让当前正在执行的线程暂停millis毫秒加nanos微秒(千分之一毫秒),并进入阻塞状态,该方法受到系统计时器和线程调度器的精度和准确性的影响。

[size=xx-small]注意:纳秒级的计时是十分不准确的,因为计算机硬件、操作系统本身无法精确到微秒。所以,程序很少调用第二种形式的sleep方法。[/size]

当当前线程调用sleep方法进入阻塞状态后,在其sleep时间段内,该线程不会获得执行的机会,即使系统中没有其他可运行的线程,处于sleep中的线程也不会运行,因此该sleep方法常用来暂停程序的执行。

例程1:
import java.util.*;
public class SleepDemo{
public static void main(String args[]){
for(int i=0;i<5;i++){
System.out.println("当前时间:"+new Date());
try{
Thread.sleep(1000);
}
catch(InterruptedException ie){
ie.printStackTrace();
}
}
}
}


程序的运行结果如下:
[color=blue]当前时间:Thu Sep 30 15:31:13 CST 2010
当前时间:Thu Sep 30 15:31:14 CST 2010
当前时间:Thu Sep 30 15:31:15 CST 2010
当前时间:Thu Sep 30 15:31:16 CST 2010
当前时间:Thu Sep 30 15:31:17 CST 2010[/color]

例程2:
class MyRunnable1 implements Runnable{
public void run(){
for(int i=0;i<5;i++){
System.out.println("["+i+"] 我是线程1!!!");
try{
Thread.sleep(1000);
}
catch(InterruptedException ie){
ie.printStackTrace();
}
}
}
}
class MyRunnable2 implements Runnable{
public void run(){
for(int i=0;i<5;i++){
System.out.println("<"+i+"> 我是线程2!!!");
try{
Thread.sleep(1000);
}
catch(InterruptedException ie){
ie.printStackTrace();
}
}
}
}
public class SleepDemo1{
public static void main(String args[]){
MyRunnable1 mr1 = new MyRunnable1();
MyRunnable2 mr2 = new MyRunnable2();
Thread t1 = new Thread(mr1);
Thread t2 = new Thread(mr2);
t1.start();
try{
Thread.sleep(10);
}
catch(InterruptedException ie){
ie.printStackTrace();
}
t2.start();
}
}

程序的运行结果如下:
[color=blue][0] 我是线程1!!!
<0> 我是线程2!!!
[1] 我是线程1!!!
<1> 我是线程2!!!
[2] 我是线程1!!!
<2> 我是线程2!!!
[3] 我是线程1!!!
<3> 我是线程2!!!
[4] 我是线程1!!!
<4> 我是线程2!!!
[/color]

例程3:
import java.util.Calendar;
import java.util.GregorianCalendar;
import javax.swing.*;

public class DigitalClock{
public static void main(String args[]){
JFrame jf = new JFrame("Clock");
JLabel clock = new JLabel("Clock");
clock.setHorizontalAlignment(JLabel.CENTER);
jf.add(clock,"Center");
jf.setSize(140,80);
jf.setLocation(500,300);
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.setVisible(true);
Thread t = new MyThread(clock);
t.start();
}
}

class MyThread extends Thread{
private JLabel clock;
public MyThread(JLabel clock){
this.clock = clock;
}
public void run(){
while(true){
clock.setText(this.getTime());
try{
Thread.sleep(1000);
}
catch(InterruptedException ie){
ie.printStackTrace();
}
}
}
public String getTime(){
Calendar c = new GregorianCalendar();
String time = c.get(Calendar.YEAR)+"-"+(c.get(Calendar.MONTH)+1)+"-"+c.get
(Calendar.DATE)+" ";
int h = c.get(Calendar.HOUR_OF_DAY);
int m = c.get(Calendar.MINUTE);
int s = c.get(Calendar.SECOND);
String ph = h<10?"0":"";
String pm = m<10?"0":"";
String ps = s<10?"0":"";
time+=ph+h+":"+pm+m+":"+ps+s;
return time;
}
}

程序的运行结果如下:

[img]http://dl.iteye.com/upload/attachment/328421/82578163-3ae0-3db3-9e13-4e9826567201.jpg[/img]

上述程序中,子线程每次循环显示一次当前系统时间后将睡眠1000ms(1s),在其睡眠期间,系统可以执行其他程序或线程,以提高运行效率。

需要说明的是,上述数字时钟的显示刷新时间间隔不一定是准确的1000ms,因为子线程在睡眠结束后要转入就绪队列中等待,而不一定能立即恢复执行,只是由于线程交替间隔都很短,人眼无法察觉罢了。

此外,子线程的线程类MyThread作为一个普通的外层类,是无法直接访问其他类中的数据的,而我们又需要其访问到主线程中的局部变量(用于显示时间信息的JLabel组件)clock,于是这里采用了参数传递的方式来实现线程间的数据共享。

上述程序的主线程在创建图形用户界面并启动子线程后就结束了,循环显示时间的工作则交给了新创建的子线程来做,当前应用程序仍只有一个用户线程在运行。这样的程序结构比较清晰,但也未免可惜——我们一方面任由主线程无事可做而退出、另一方面又要创建新的线程去做事,而创建和销毁线程的操作是要增大运行开销的。实际上,这种情况下。我们可以将子线程的任务直接安排给主线程来做。

修改上述例程3后的程序如下:
import java.util.Calendar;
import java.util.GregorianCalendar;
import javax.swing.*;

public class DigitalClock{
public static void main(String args[]){
JFrame jf = new JFrame("Clock");
JLabel clock = new JLabel("Clock");
clock.setHorizontalAlignment(JLabel.CENTER);
jf.add(clock,"Center");
jf.setSize(140,80);
jf.setLocation(500,300);
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.setVisible(true);
Thread t = new MyThread(clock);
t.start();

while(true){
clock.setText(getTime());
try{
Thread.sleep(1000);//this.sleep(1000);
}
catch(InterruptedException ie){
ie.printStackTrace();
}
}
}

public static String getTime(){
Calendar c = new GregorianCalendar();
String time = c.get(Calendar.YEAR)+"-"+(c.get(Calendar.MONTH)+1)+"-"+c.get
(Calendar.DATE)+" ";
int h = c.get(Calendar.HOUR_OF_DAY);
int m = c.get(Calendar.MINUTE);
int s = c.get(Calendar.SECOND);
String ph = h<10?"0":"";
String pm = m<10?"0":"";
String ps = s<10?"0":"";
time+=ph+h+":"+pm+m+":"+ps+s;
return time;
}
}

运行结果同例程3。

[size=medium]2、线程的优先级[/size]

很多系统在对进程进行调度时,会采用优先级调度策略。Java中在对线程进行调度时,也采用了优先级调度策略,具体策略为:“优先级高的线程应该有更大的获取CPU资源而执行的概率,优先级低的线程并不是总不能执行”。也就是说,当前正在执行的线程优先级一般不会比正在准备状态等待执行的线程优先级低。

[size=xx-small]提示:从Java的优先级策略中可以看出,哪个线程先执行是没有保障的。因此,优先级策略不适合对线程进行细度调度,而只是适合进行宏观调控。例如,运算量大的计算线程优先级设置的低一些,响应用户输入的线程优先级高一些,以改善程序的响应性能。[/size]

Java中线程的优先级用1~10之间的整数表示,数值越大优先级越高,默认优先级为5。在没有特别指定的情况下,主线程的优先级为5。另外,对于子线程,其初始优先级与其父线程的优先级相同。也就是说,若父线程的优先级为8,则其子线程的初始优先级为8。

[size=xx-small]提示:Java中的线程优先级是依赖于本地平台的,在实际运行时会将线程在Java中的优先级映射到本地的某个优先级。这样,如果本地提供的优先级比10个要少,则Java中不同的优先级可能会映射成相同的本地优先级,而具有基本相同的执行概率。[/size]

Thread类提供了以下方法来获得和设置线程对象的优先级。

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

(2)public final void setPriority(int newPriority);
设置当前线程对象的优先级。

其中,newPriority表示需要设置的优先级别,应该是1~10之间的整数。为了便于记忆,Java中提供了3个静态常量来表示比较常用的优先级别:

(1)public static final int MAX_PRIORITY:表示最高优先级(10)
(2)public static final int MIN_PRIORITY:表示最低优先级(1)
(3)public static final int NORM_PRIORITY:表示默认优先级(5)

例程4:
public class PriorityDemo{
public static void main(String args[]){
System.out.println("线程名 优先级");
Thread current = Thread.currentThread();
System.out.println(current.getName()+" "+current.getPriority());
Thread t1 = new MyThread1();
Thread t2 = new MyThread1();
Thread t3 = new MyThread1();
t1.setName("线程1");
t2.setName("线程2");
t3.setName("线程3");
t2.setPriority(Thread.MAX_PRIORITY);
t3.setPriority(8);
t1.start();
t2.start();
t3.start();
}
}
class MyThread1 extends Thread{
public void run(){
System.out.println(this.getName()+" "+this.getPriority());
}
}


程序的运行结果如下(结果不唯一):
[color=blue]线程名 优先级
main 5
线程2 10
线程3 8
线程1 5[/color]

例程5:
class MyThread2 extends Thread{
public MyThread2(String name){
super(name);
}
public void run(){
for(int i=0;i<10;i++){
System.out.print(this.getName()+i+" ");
}
}
}
public class PriorityDemo1{
public static void main(String args[]){
Thread t1 = new MyThread2("线程<1>");
Thread t2 = new MyThread2("线程<2>");
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
}
}


程序的运行结果如下(结果不唯一):

[color=blue]线程<1>0 线程<2>0 线程<2>1 线程<2>2 线程<2>3 线程<2>4 线程<2>5 线程<2>6 线程<2>7线程<2>8 线程<1>1 线程<1>2 线程<1>3 线程<1>4 线程<1>5 线程<1>6 线程<1>7 线程<2>9 线程<1>8 线程<1>9[/color]

从以上两个程序可以看出:即使两个线程设置了不同的优先级,也无法准确地保证线程的执行顺序。

例程6:
public class PriorityDemo2 extends Thread{
public PriorityDemo2(String name){
super(name);
}
public void run(){
for(int i=1;i<=5;i++){
System.out.println(getName()+",其优先级为:"+getPriority()+",循环变量的值为:"+i);
}
}
public static void main(String args[]){
Thread.currentThread().setPriority(6);
for(int i=1;i<=15;i++){
if(i==5){
PriorityDemo2 low = new PriorityDemo2("低级");
low.start();
System.out.println("创建之初的优先级:"+low.getPriority());
low.setPriority(Thread.MIN_PRIORITY);
}
if(i==10){
PriorityDemo2 high =new PriorityDemo2("高级");
high.start();
System.out.println("创建之初的优先级:"+high.getPriority());
high.setPriority(Thread.MAX_PRIORITY);
}
}
}
}


程序的运行结果如下(结果不唯一):

[color=blue]创建之初的优先级:6
创建之初的优先级:6
低级,其优先级为:6,循环变量的值为:1
低级,其优先级为:1,循环变量的值为:2
低级,其优先级为:1,循环变量的值为:3
高级,其优先级为:6,循环变量的值为:1
高级,其优先级为:10,循环变量的值为:2
高级,其优先级为:10,循环变量的值为:3
高级,其优先级为:10,循环变量的值为:4
高级,其优先级为:10,循环变量的值为:5
低级,其优先级为:1,循环变量的值为:4
低级,其优先级为:1,循环变量的值为:5[/color]

[size=medium]3、join线程[/size]

Thread类提供了让一个线程等待另一个线程完成的方法:join()方法。当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join()方法加入join线程完成为止。

join()方法通常由使用线程的程序调用,以将大问题划分成许多小问题,每个小问题分配一个线程。当所有的小问题都得到处理后,在调用主线程来进一步操作。

join()方法有三种重载的形式:

● public final void join() throws InterruptedException
等待被join的线程执行完成。

● public final void join(long millis) throws InterruptedException
等待被join的线程的时间最长为millis毫秒。如果在millis毫秒内,被join的线程还没有执行结束则不再等待。

● public final void join(long millis,int nanos) throws InterruptedException
等待被join的线程的时间最长为millis毫秒加上nanos微秒。

[size=xx-small]注意:通常第三个方法很少使用,原因有两个:程序对时间的精度无需精确到千分之一毫秒;计算机硬件、操作系统本身也无法精确到千分之一毫秒。[/size]

例程7:
class MyRunnable3 implements Runnable{
public void run(){
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+i);
}
System.out.println("线程"+Thread.currentThread().getName()+"执行结束!");
}
}
public class JoinDemo{
public static void main(String args[]){
MyRunnable3 mr = new MyRunnable3();
Thread t = new Thread(mr,"新线程");
t.start();
try{
System.out.println("使用join()方法");
t.join();
}
catch(InterruptedException ie){
ie.printStackTrace();
}
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+i);
}
}
}


程序的运行结果如下:

[color=blue]使用join()方法
新线程0
新线程1
新线程2
新线程3
新线程4
新线程5
新线程6
新线程7
新线程8
新线程9
线程新线程执行结束!
main0
main1
main2
main3
main4
main5
main6
main7
main8
main9[/color]

分析:上述程序的主线程在执行的过程中调用了线程t的join()方法,该方法将导致当前线程(主线程)阻塞,直到线程t运行终止,主线程才会获得继续执行的机会。这相当于将线程t串行加入到主线程中,即将两条齐头并进的运行线索合并为一条。

既然逻辑上是按一条线索运行的任务,为什么要定义两个线程,直接定义一个不是更好吗?问题是,如果我们要重用已有的程序代码,而这些代码当初是以独立线程的方式开发的,我们如果不想大做改动,就只好采取这种线程“合并”的方式了。

例程8(将例程7稍作修改):
class MyRunnable3 implements Runnable{
public void run(){
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+i);
}
System.out.println("线程"+Thread.currentThread().getName()+"执行结束!");
}
}
public class JoinDemo{
public static void main(String args[]){
MyRunnable3 mr = new MyRunnable3();
Thread t = new Thread(mr,"新线程");
t.start();
for(int i=0;i<10;i++){
if(i==5){
try{
System.out.println("使用join()方法");
t.join();
}
catch(InterruptedException ie){
ie.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+i);
}


}
}


程序的运行结果如下:
结果1:

[color=blue]main0
main1
main2
新线程0
main3
新线程1
新线程2
main4
新线程3
[b]使用join()方法[/b]
新线程4
新线程5
新线程6
新线程7
新线程8
新线程9
[b]线程新线程执行结束![/b]
main5
main6
main7
main8
main9[/color]

结果2:

[color=blue]main0
新线程0
新线程1
新线程2
新线程3
新线程4
新线程5
新线程6
新线程7
main1
新线程8
main2
新线程9
main3
[b]线程新线程执行结束[/b]!
main4
[b]使用join()方法[/b]
main5
main6
main7
main8
main9[/color]

例程9:
public class JoinDemo1 extends Thread{
public JoinDemo1(String name){
super(name);
}
public void run(){
for(int i=1;i<=5;i++){
System.out.println(getName()+i);
}
}
public static void main(String args[]){
new JoinDemo1("新线程").start();
for(int i=1;i<=10;i++){
if(i==5){
JoinDemo1 j = new JoinDemo1("被join的线程");
j.start();
try{
j.join();
}
catch(InterruptedException ie){
ie.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+i);
}
}
}

程序的运行结果如下(结果不唯一):
[color=blue]main1
新线程1
main2
新线程2
main3
新线程3
main4
新线程4
被join的线程1
新线程5
被join的线程2
被join的线程3
被join的线程4
被join的线程5
[b]main5[/b]
main6
main7
main8
main9
main10[/color]

[size=medium]4、线程让步:yield[/size]

yield()方法是一个和sleep()方法有点类似的方法,它也是一个Thread类提供的一个[b]静态[/b]方法,它也可以让当前正在执行的线程暂停,但它不会阻塞该线程,它只是将该线程转入就绪状态。yield只是让当前线程暂停一下,让系统的线程调度器重新调度一次,完全可能的情况是:当某个线程调用了yield()方法暂停之后,线程调度器有将其调度出来重新执行,也就是说yield让步不一定成功。

yield()方法的形式如下:
public static void yield()

实际上,当某个线程调用了yield()方法暂停之后,只有优先级与当前线程相同,或者优先级比当前线程更高的就绪状态的线程才能获得执行机会。

例程10:
class MyThread3 extends Thread{
private boolean flag;
public MyThread3(String name,boolean flag){
super(name);
this.flag = flag;
}
public void setFlag(boolean flag){
this.flag = flag;
}
public void run(){
long start = System.currentTimeMillis();
for(int i=0;i<20;i++){
if(flag){
Thread.yield();
}
System.out.print(this.getName()+i+" ");
}
long end = System.currentTimeMillis();
System.out.println();
System.out.println(this.getName()+"执行时间:"+(end - start)+"ms");
}
}
public class YieldDemo{
public static void main(String args[]){
Thread t1 = new MyThread3("First",false);
Thread t2 = new MyThread3("Second",true);
Thread t3 = new MyThread3("Three",false);
t1.start();
t2.start();
t3.start();
}
}


程序的运行结果如下:
[color=blue]First0 First1 First2 Second0 First3 Second1 First4 Second2 First5 First
6 First7 Second3 Three0 Three1 Three2 Three3 Three4 Three5 Three6 Three7 Three8 Three9 Three10 Three11 Three12 Three13 Three14 Three15 Three16 Three17 Three18 Three19
[b]Three执行时间:3ms[/b]
First8 First9 Second4 Second5 Second6 Second7 Second8 Second9 Second10 First10 First11 First12 First13 First14 First15 First16 First17 First18 First19 Second11
[b]First执行时间:9ms[/b]
Second12 Second13 Second14 Second15 Second16 Second17 Second18 Second19
[b]Second执行时间:11ms[/b][/color]

分析:由于Second线程执行了让步操作,其运行的时间长于其他两个线程。但是以上的运行结果也是不唯一的,Second线程的运行时间在每次运行时的运行时间也不是绝对长于其他两个线程,另一组结果如下:
[color=blue]First0 First1 Second0 Second1 First2 First3 First4 First5 First6 First7 Three0 Three1 Three2 Three3 Three4 Three5 Three6 Three7 Three8 Three9 First8 First9 First10 First11 First12 First13 First14 First15 First16 First17 First18 First19 Second2 Three10
[b]First执行时间:4ms[/b]
Second3 Three11 Second4 Second5 Second6 Second7 Second8 Second9 Second10 Second11 Second12 Second13 Second14 Second15 Second16 Second17 Three12 Second18 Second19 Three13
[b]Second执行时间:8ms[/b]
Three14 Three15 Three16 Three17 Three18 Three19
[b]Three执行时间:10ms[/b][/color]

例程11:
public class YieldDemo1 extends Thread{
public YieldDemo1(String name){
super(name);
}
public void run(){
for(int i=0;i<10;i++){
System.out.println(this.getName()+i);
if(i==5){
Thread.yield();
}
}
}
public static void main(String args[]){
YieldDemo1 j1 = new YieldDemo1("高级");
j1.setPriority(Thread.MAX_PRIORITY);
j1.start();
YieldDemo1 j2 = new YieldDemo1("低级");
j2.setPriority(Thread.MIN_PRIORITY);
j2.start();

}
}

程序的运行结果如下(结果不唯一):
[color=blue]高级0
高级1
低级0
高级2
低级1
高级3
高级4
低级2
[b]高级5[/b]
低级3
高级6
低级4
高级7
高级8
高级9
[b]低级5[/b]
低级6
低级7
低级8
低级9[/color]

将例程11稍作修改:将程序中两条设置线程优先级的语句注释掉后,程序的结果如下:
[color=blue]高级0
低级0
低级1
高级1
低级2
低级3
高级2
低级4
高级3
[b]低级5[/b]
高级4
低级6
低级7
[b]高级5[/b]
低级8
高级6
低级9
高级7
高级8
高级9[/color]

[b][size=medium]关于sleep方法和yield方法的区别:[/size][/b]

(1)sleep方法暂停当前线程后,会给其他线程执行机会,不会理会其他线程的优先级。但yield方法只会给优先级相同,或优先级更高的线程执行机会。

(2)sleep方法会将线程转入阻塞状态,直到经过阻塞时间才会转入就绪状态。而yield方法不会将线程转入阻塞状态,它只是强制当前线程进入就绪状态。因此完全有可能某个线程调用yield方法暂停之后,立即再次获得处理器资源而被执行。

(3)sleep方法声明抛出了InterruptedException异常,所以调用sleep方法时要么捕捉该异常,要么显式声明抛出该异常。而yield方法则没有声明抛出任何异常。

(4)sleep方法比yield方法有更好的可移植性,通常不要依靠yield来控制并发线程的执行。


[size=medium]5、后台线程[/size]

后台线程(Background Thread)是指那些在后台运行的、为其他线程提供服务的线程,如JVM的垃圾回收线程、起计时器功能的定时器线程等,后台线程也称守护线程(Daemon Thread)。和后台线程想对应的是,其他完成用户指定任务的线程也可称为“用户线程”,如运行java应用程序时,JVM自动调用程序的入口方法main()方法所产生的线程,该线程也称为主线程(Main Thread)。

后台线程有个特征:当所有的前台线程都死亡,后台线程就会自动死亡。

Thread类提供的与后台线程有关的方法包括:

● public final boolean isDaemon()
测试当前线程是否为守护线程,如是则返回true,否则返回false。

● public final void setDaemon(Boolean on)
将当前线程标记为守护线程或用户线程,本方法必须在启动线程前调用。

例程12:
class MyCommon implements Runnable{
public void run(){
for(int i=0;i<10;i++){
System.out.println("["+i+"]");
}
System.out.println("{前台用户线程执行完毕!}");
}
}

class MyDaemon implements Runnable{
public void run(){
for(int i=0;i<1000;i++){
System.out.println("<"+i+">");
}
System.out.println("{后台守护线程执行完毕!}");
}
}

public class DaemonDemo{
public static void main(String args[]){
Runnable mc = new MyCommon();
Runnable md = new MyDaemon();
Thread tc = new Thread(mc);
Thread td = new Thread(md);
td.setDaemon(true);
tc.start();
td.start();
}
}

程序的运行结果如下:
[color=blue][0]
[1]
[2]
[3]
[4]
[5]
[6]
[7]
[8]
[9]
{前台用户线程执行完毕!}
<0>
<1>
<2>
<3>
<4>
<5>
<6>
<7>
<8>[/color]

从运行的结果可以看出,前台用户线程是保证执行完毕的,而后台守护线程并没有运行结束(只打印到8,离999还差很远),只是因为java运行时环境判断程序是否结束的标准是:“是否所有的前台用户线程是否都执行完毕了,如果所有的前台线程都执行完毕,程序退出”。

[size=xx-small]注意:主线程默认是前台线程,线程mc和md默认也是前台线程。并不是所有的线程默认都是前台线程,有些线程默认是后台线程:前台线程创建的子线程默认是前台线程,后台线程创建的子线程默认是后台线程。[/size]
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值