您的位置:首页 > 编程语言 > Java开发

java 的线程模型

2012-11-12 17:55 363 查看
在运行java时,系统在许多方面都依赖于多线程,而且在设计所有的类库时也要考虑到多线程。事实上,java是使用线程以便整个环境异步,以便减少cpu周期的浪费来提高效率。 通过对单线程环境对比,多线程环境的优势可以得到更好的理解。单线程系统使用轮流检测(polling)事件循环的方法,控制线程运行无限次循环,轮流检测一个事件队列中每个事件以便确定下一不做什么。一但检测机制返回一个信号,说明网络文件已经准备好并可以读了,那么事件循环分发是控制相应事件的处理器,而系统在这个事件返回之前什么也不能做。这就使CPU浪费时间,也会导致程序的一部分占有整个系统,从而妨碍其他事件被系统处理,总的来说,总的来说,当一个线程因为等待某些资源而阻塞(延迟运行)时,整个系统将停止运行。java多线程就是取消了主循环和轮流检测机制,一个线程可以暂停而不妨碍系统其他部分运行。在java中一个线程被阻塞时,只有那个线程被阻塞,其他所有线程继续执行。

线程存在几个状态:线程可以是运行状态(running);只获得cup时间成为准备状态(ready to run );运行中的线程还可以有暂停状态(suspended), 暂时停止运行;暂停的线程能被重新启动,允许在暂停的地方从新激活;线程在等待其他资源时称为阻塞状态(blocked);在任何时候,线程都能被终止,并立即停止他的活动;线程一旦停止,就不能重新激活。

1、线程的优先级

java给每个线程分配一个优先级,以决定线程相对其他线程来说如何被处理。线程优先级是整数,用于指定线程的相对优先程度,所以优先级作为一个绝对值是没有意义的。如果是唯一运行的线程,高优先级的线程并不比低优先级的线程运行的快。同时线程的优先级能够决定什么时候从一个运行的线程切换到另一个线程,这称为上下文切换(context switch) 。 决定什么时候进行上下文切换的规则简单:

一个线程自愿的放弃控制:这通过线程的放弃、睡眠或者i/o阻塞来完成。这种情况下,所有其他线程接受检测,高优先级的线程被分配cpu时间。

一个线程可以被一个高优先级的线程抢占资源:在这种情况下,低优先机线程得不到处理,不管它做什么,处理器总是被高优先级的线程占用:高优先级的线程想运行就能运行。这称为抢占式多任务。

如果是两个线程具有同一个优先级并且竞争cpu时间,情况有点复杂。对windows98系统来说,相同优先级的线程会通过自动循环的方式获得时间cpu时间片断。对其他类型的操作系统,相同优先级的线程如果不自愿放弃控制权限给其他线程,其他线程将得不到运行。

2、同步

因为多线程将异步行为引进程序,必须有一种方法强制进行。例如,如果两个线程想要通信并且共享一个复杂的数据资源,如链表,此时需要一些方法以确保它们互不冲突,也就是必须阻止一个线程在在另一个线程读数据时写入数据,为了达到这个目的,java在线程之间建立了同步模型 —— 监控器(Monitor)的基础上实现了一个巧妙的方案:监控器是一个监控机制,可以认可一个很小、只能容纳一个线程的盒子,一但线程进入监控器,所有其他线程必须等待,直到那个线程退出为止。一但线程进入同步方法,其他线程就不能调用该对象的任何其他方法。

3、消息

在将程序分为独立的程序后,需要定义线程之间如何进行相互交流。java提供了低代价的两个或者多个线程之间交流的方法:调用所有对象都有预定义方法。java的消息系统允许一个线程进入一个对象的同步方法之内。并在那里等待,直到其他线程通知它出来。

4、Thread 类 和Runnable 接口

java的多线程系统构建在Thread类、方法以及Runnable接口之上。Thread类封装了线程的执行,如果不能直接引用一个运行线程的状态,可以通过一个产生它的线程的实例来处理他,通过继承Thread类 或者实现Runnable接口创建一个新线程。Thread类定义了几个管理线程的方法如下:

getName 获得线程名字

getPriority 获得线程优先级

jsAlive 判断线程是否仍在运行

join 等待一个线程终止

run 线程入口

sleep 暂停一个线程一段时间

start 通过调用线程的运行方法启动它

5、主线程

当java启动时,一个线程立刻开始运行。这个线程称为程序的主线程,因为它是程序开始执行的线程。主线程重要体现在两个方面:

1、它是产生其他子线程的线程。

2、一般情况下,必须是最后一个结束执行的线程,因为它要执行各种关闭操作 。

主线程不但在程序可以自动创建,也能通过Thread类控制,此时需要调用currentThread()方法,这是Thread的公共静态方法。它的一般形式:static Thread currentThread() 这个方法在调用它的地方返回一个线程引用。一旦获得对主线程的调用,就能象控制其他线程一样控制它。看下面例子:

class CurrentThreadDemo {
public static void main(String[] args) {
Thread t = Thread.currentThread();
System.out.println("Current thread " +t);
//change the name of the thread
t.setName("My Thread");
System.out.println("Atfter name change : "+ t);

try {
for(int n = 5 ; n >0 ; n--){
System.out.println(n);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.println("Main thread InterruptedException ");
}
}
}


在这个程序中、对当前线程(在这里是主线程)的引用通过currentThread()方法获得,并引用保存在本地变量t中。程序先显示了该线程存在的信息,接着调用setName()方法来改变线程的名字,并在显示线程信息,然后将一个循环从5开始递减,在每次执行之间间隔1秒。停顿通过sleep()方法,该方法抛出一个InterruptedException异常。如果其他一些线程想要终止休眠状态,这个异常就会发生。输出结果:

Current thread Thread[main,5,main]

Atfter name change : Thread[My Thread,5,main]

5

4

3

2

1

注意t在println()中产生的输出:它按照“线程名字”、“优先级”、“组的名字”顺序来输出。默认情况下,主线程名字:main ,优先级默认是5 , 组名字

main (线程组是一个整体控制线程集合的数据结构)。线程名字改变后,t被重新输出。

sleep()方法使线程从调用时起暂缓执行一定的毫秒时间,它的通用形式:

static void sleep (long ,milliseconds)throws InterruptedException

暂停时间以毫秒为单位。这个方法抛出InterruptedException异常。sleep()方法还有第二个形式。如下。允许用毫秒和纳秒指定时间间隔。

static void sleep(long milliseconds,int nanoseconds)throws InterruptedException

第二种形式只用于允许时间短到纳秒的环境。

setName()设置线程名字,可以调用getName()获得线程名字(注意这个方法没有在线程中显示),这些方法是Thread成员函数,声明如下:

final void setName(String threadName)

final String getName()

6、创建一个线程

一般情况下,通过实例化一个Thread类对象来创建线程。java定义了两种创建线程方法:

1、可以实现Runnable接口。

2、可以派生Thread类

实现Runnable接口

创建线程的最简单方法就是创建一个实现Runnable接口的类,Runnable接口抽象了可执行代码单元,可以通过任何实现Runnable接口的对象来构造线程。实现Runnable只需要一个简单的run()方法,他的声明如下:

public void run()

在run()方法中,可以定义构成新线程的代码。run()方法能够象主线程一样调用其他方法、使用其他类和声明变量,唯一区别是run()方法是程序中的另一个执行线程的进入点,这个线程在run()方法返回是结束。

创建一个实践Runnable()接口的类以后,就可以在类中实例化Thread类的对象了,Thread类构造了几个构造函数:

Thread (Runnable threadOb,String threadName)

在这个构造函数中,threadOb是实现Runnable接口的一个实例,它定义了线程执行从哪里开始。新线程名字有threadName指定。新线程创建以后,知道调用了他的start()方法后才执行。本质上,start()执行对run()调用。start()方法如下:

void start()

下面是创建一个线程并运行它的例子。

class NewThread implements Runnable {
Thread t ;
NewThread(){
t = new Thread(this,"Demo Thread");
System.out.println("Child thread : " +t);
t.start();

}
public void run(){
try {
for(int i = 5 ; i >0 ; i--){
System.out.println("Child Thread: " +i);
Thread.sleep(500);
}
} catch (InterruptedException e) {
System.out.println("Child interrupted.");
}
System.out.println("Exiting child thread.");
}
}
class ThreadDemo {
public static void main(String[] args) {
new NewThread();
try {
for(int i = 5 ; i>0 ;i--){
System.out.println("Main Thread: "+i);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.println("Main thread interrupted.");
}
System.out.println("Main thread exiting.");
}
}


在NewThread的构造函数中,一个新的线程对象通过如下语句创建:

t = new Thread(this,"Demo Thread");

第一个参数this表示希望新线程调用this上的run()方法,然后在调用start(),也就是从run()方法开始启动线程的执行。调用start()之后,NewThread的构造函数返回到main()函数中,主线程从新激活并进入for循环。两个线程继续运行,共享cpu直到循环结束。产生如下输入:

Child thread : Thread[Demo Thread,5,main]

Main Thread: 5

Child Thread: 5

Child Thread: 4

Main Thread: 4

Child Thread: 3

Child Thread: 2

Main Thread: 3

Child Thread: 1

Exiting child thread.

Main Thread: 2

Main Thread: 1

Main thread exiting.

在多线程程序中,主线程必须最后执行。在上面的程序中,主线程最后结束,因为主线程每次休眠1000毫秒,而子线程每次休眠500毫秒。

扩展线程

第二种创建线程的方法是创建一个扩展Thread类的新类,然后创建该类的一个实例,这个扩展的类必须重写run()方法,这是新线程的进入点,同时它也必须调用start()方法来执行新线程,下面的程序是改写的前一个程序,以扩展Thread类:

class NewThread extends Thread{

NewThread(){

super("Demo Thread");

System.out.println("Child Thread: " +this);

start();

}

public void run(){

try {

for(int i = 5 ; i >0 ; i--){

System.out.println("Child Thread: " +i);

Thread.sleep(500);

}

} catch (InterruptedException e) {

System.out.println("Child interrupted.");

}

System.out.println("Exiting child thread.");

}

}

class ThreadDemo {

public static void main(String[] args) {

new NewThread();

try {

for(int i = 5 ; i>0 ;i--){

System.out.println("Main Thread: "+i);

Thread.sleep(1000);

}

} catch (InterruptedException e) {

System.out.println("Main thread interrupted.");

}

System.out.println("Main thread exiting.");

}

}

这个与前面一个程序输出相同,可以看到,子线程通过实例化NewThread类(一个Thread类的派生类)来创建线程。请注意NewThread类中对super()方法的调用,它将调用如下构造函数:public Thread (String threadName);

threadName是指定线程名字。

选择一种方法

前面可以看出,java有两种方法可以创建子线程,那么哪种更好呢?Thread类定义了几个派生类可以重写的方法,其中一个必须被重写的方法是run(),它也在实现Runnable接口所必须的方法。因此只有在以某种方式增强或者修改Thread类时才应该派生Run(),因此,如果不想重写Thread类的其他方法,最好还是简单的调用Runnable接口。

7、创建多线程

到现在为止,只用过两个线程:主线程和一个子线程。但实际上程序可能启动它所需要的很多线程。下面是创建3个子线程:

class NewThread implements Runnable {
String name;
Thread t ;

NewThread(String nameThread){
name = nameThread;
t = new Thread(this,name);
System.out.println("NewThread : "+t);
t.start();
}
public void run(){
try {
for(int i = 5 ;i>0 ; i--){
System.out.println(name+":"+i);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.println(name +" InterruptException");
}

System.out.println(name+" exiting");
}

}
class MultiThreadDemo {
public static void main(String[] args) {
new NewThread("one");
new NewThread("two");
new NewThread("three");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
System.out.println("Main thread Interrupted");
}
System.out.println("Main thread exiting");
}
}


输出结果:

NewThread : Thread[one,5,main]

NewThread : Thread[two,5,main]

NewThread : Thread[three,5,main]

one:5

two:5

three:5

three:4

two:4

one:4

one:3

two:3

three:3

one:2

three:2

two:2

two:1

three:1

one:1

two exiting

one exiting

three exiting

Main thread exiting

可以看出一启动,3个子线程共享cpu。注意main()函数中对sleep()的调用,是为了以确保主线程最后结束。

8、使用isAlive()和join()

如前所述,主线程一般要最后结束。在上面的例子中,它是通过main()中调用sleep()方法来实现的,使主线程休眠足够长的时间以确保子线程先终止。然而,这不是令人满意的解决方法,它有个严重问题:怎么样知道另一个线程终止是否终止?在Thread类中提供了解决这问题的方法。有两种方法确定线程是否终止。第一,可以在主线程中调用isAlive(),这方法是Thread类定义的,它的一般形式如下:

final boolean isAlive()

如果它调用的线程仍在运行,isAlive()返回true,否则返回false。

虽然isAlive很有用,一个更常使用的方法是调用join()方法来等待另一个线程的结束。它的一般形式:

final void join() throws InterruptException

这个方法一直等待,直到它调用的线程终止,join()的另一个形式允许指定等待线程的最大时间。下面类子的改进,它运用join()方法来确保主线程最后终止,同时也演示了isAlive()的使用。

class NewThread implements Runnable {
String name;
Thread t ;
NewThread(String name){
this.name =name;
t = new Thread(this,name);
System.out.println("New Thread: "+name);
t.start();
}
public void run(){
try {
for(int i = 5 ; i>0 ;i--){
System.out.println(name +": "+ i);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.println(name+ " Interrupted");
}
System.out.println(name +" exiting");
}
}
class Demojoin {
public static void main(String[] args) {
NewThread ob1 = new NewThread("one");
NewThread ob2 = new NewThread("two");
NewThread ob3 = new NewThread("three");
System.out.println("Thread one is alive : "+ob1.t.isAlive());
System.out.println("Thread two is alive : "+ob2.t.isAlive());
System.out.println("Thread three is alive : "+ob3.t.isAlive());
try {
System.out.println("Waiting for thread to finish.");
ob1.t.join();
ob2.t.join();
ob3.t.join();
} catch (InterruptedException e) {
System.out.println("Main thread InterruptException");
}
System.out.println("Thread one is alive : "+ob1.t.isAlive());
System.out.println("Thread two is alive : "+ob2.t.isAlive());
System.out.println("Thread three is alive : "+ob3.t.isAlive());
System.out.println("Main thread exiting");
}
}


输出结果:

New Thread: one

New Thread: two

New Thread: three

one: 5

two: 5

Thread one is alive : true

Thread two is alive : true

Thread three is alive : true

Waiting for thread to finish.

three: 5

one: 4

two: 4

three: 4

one: 3

two: 3

three: 3

one: 2

two: 2

three: 2

one: 1

two: 1

three: 1

one exiting

two exiting

three exiting

Thread one is alive : false

Thread two is alive : false

Thread three is alive : false

Main thread exiting

可以看到在调用join()方法返回后,线程已经停止执行。

9、线程的优先级

线程调度器使用线程的优先级决定线程什么时候允许运行。从理论上来说,高优先级的线程比低优先级的线程得到的cpu时间更多,如果要设置线程优先级,可以使用setPriority()方法。它是Thread类中的的成员,其通用形式如下:

final void setPriority(int level)

这里level用来指定调用线程的新优先级 。level值必须是在MIN_PRIORITY和MAX_PRIORITY之间(分别是 1 和 10 ) 。NORM_PRIORITY用来指定线程优先级的为一般值(当前为5)。这些优先级在Thread中定义为final变量,可以调用Thread类的getPriority()方法调用得到当前优先级,形式如下:

final int getPriority()

下面的例子是两个不同优先级的线程,一个线程设置了优先级高于正常优先级两级,一个线程设置低于正常优先级两级,正常优先级由Thread.NOPM_PRIORITY所定义,线程被启动并允许运行10秒,每个线程设置一个循环,记下循环的次数。10秒后,主线程停止这两个线程,每个线程在cup所获得的时间的值被显示出来。

class clicker implements Runnable {
int click = 0 ;
Thread t;
private boolean running = true;
clicker(int p ){
t = new Thread(this);
t.setPriority(p);
}
public void run(){
while(running){
click++ ;
}

}
public void stop(){
running = false;
}
public void start(){
t.start();
}
}
class HiLopri {
public static void main(String[] args) {
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
clicker h1 = new clicker(Thread.NORM_PRIORITY+2);
clicker lo = new clicker(Thread.NORM_PRIORITY-2);
h1.start();
lo.start();
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
System.out.println("Main thread interrupted.");
}
h1.stop();
lo.stop();
try {
h1.t.join();
lo.t.join();
} catch (InterruptedException e1) {
System.out.println("InterruptedException caught");
}
System.out.println("Low-priority thread;" +lo.click);
System.out.println("High-priority thread;" +h1.click);
}
}


程序在windows下运行输出如下显示,可以看到线程经过了上下文转换,而且两者没有自愿放弃cpu或者等候i/o,高优先级线程获得了 90 %的cpu时间。

Low-priority thread;57755405

High-priority thread;-4752188

当然,程序产生输出依赖于cpu的速度和系统运行的其他任务数目。当同一个程序在非抢占的平台下运行时,可以获得不同的结果。

10、同步

当两个或多个线程需要访问一个共享资源时,需要某种方式来保证资源在某一时间内只被一个线程使用,这个方式称为同步。

同步的关键是监控器(Monitor)的概念,也称为信号量(semaphore)一个监控器是作为互斥排它锁(mutex)使用的一个对象:给定时刻,只可能有一个拥有该监控器。当一个线程获得了锁,也就是进入了监控器,所有其他试图进入的线程都被暂停,直到第一个线程退出监控器。

使用同步

在java中同步很简单,所有对象都有和自己关联的隐含的监控器,如果要进入一个对象的监控器,只需要调用synchronized关键词修饰的方法。当一个线程进入同步方法时,所有其他试图调用同一个实例中的该方法(或者其他同步方法)的线程必须等待。监控器的拥有者退出监控器后,会把监控器释放给下一个等待的线程。

为了理解同步,我们举一个需要使用同步的例子。现在的程序有三个简单类:第一个Callme类有一个call()方法,call()有一个String类型的参数msg,该方法方括号中打印msg字符串。在call()打印左括号和msg字符串后,调用了Thread.sleep(1000)来暂停线程的执行1秒;下一个类的构造函数Caller带有Callme类的实例的一个引用和一个字符串,分别保存在target和msg变量中,构造函数同时还创建了一个调用该对象run()方法的新线程,该线程会立即启动,然后调用Caller的run()方法调用Callme实例target的call()方法来传入msg字符串;最后,Synch类创建一个Callme实例和三个Call实例,每个实例都有一个唯一的消息字符串。Callme实

例被传送到每个Caller实例的过程相同。

class Caller implements Runnable {
String msg;
Callme target;
Thread t ;
public Caller(Callme targ,String s){
target = targ;
msg = s;
t = new Thread(this);
t.start();

}
public void run(){
target.call(msg);
}
}
class Callme {
void call(String msg){
System.out.print("[" +msg);
try{
Thread.sleep(1000);
}catch(InterruptedException e){
System.out.println("Interrupted");
}
System.out.print("]");
}
}
class Synch {
public static void main(String[] args) {
Callme target = new Callme();
Caller ob1 = new Caller(target,"Hello");
Caller ob2 = new Caller(target,"Synchronized");
Caller ob3 = new Caller(target,"world");
try {
ob1.t.join();
ob2.t.join();
ob3.t.join();
} catch (InterruptedException e) {
System.out.println("Interrupter");
}
}
}


程序输出:

[Hello[Synchronized[world]]]

可以看到,call()方法通过调用sleep()允许执行转换到另一个线程,结果是3个字符串的混合输出。在这个过程中,不存在所有三个线程同时对同一个对象调用的限制(这也称为竞争状态)。本例利用sleep()方法使得效果更明显。在大多数情况下,竞争条件是微妙的和不可预期的,不能确定上下文切换在什么时间发生,所以这会导致程序这一次正确,而下一次却错误。

为了避免这情况,必须对call()的访问进行序列话,也就是说必须限制某一时刻只有一个线程访问他。为了达到这个目的,只需要简单在call()的定义前面加上关

键词synchronized,如下所示:

calss Callme{

synchronized void call(String msg){

当有别的线程使用该方法时,它将阻止进入call()方法。synchronized void call(String msg) 的输出结果 :

[Hello][Synchronized][world]

所以在多线程的情况下,如果有一个方法或者一组方法需要操纵对象的内部状态,就应该使用synchronized关键词来保护状态不处于竞争状态。记住,一旦一个线程进入一个实例的任何同步方法,别的线程将不能进入同一实例同步方法,但是该实例的非同步方法仍然能够被调用。

11、同步语句

在类中创建synchroized方法是一种取得同步的简单有效方法,它并不是所有情况下都有效,假如你想同步访问没有设计为多线程访问类的方法的类的对象,也就说,类不能使用sysnchroized方法,解决这问题的方法:只需要将对该类方法的访问防于一个同步块中,下面是同步语句的形式:

synchronized(objct){

//statement to be synchroized

}

这里objct是需要同步的对象的引用。使用一个同步块,可以确保只有当前线程进入被同步对象的控制器才可以访问对象的成员方法。下面代码在run()方法中使用了同步块:

class Callme {
void call(String msg){
System.out.print("["+msg);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
System.out.println("]");
}
}
class Caller implements Runnable {
String msg;
Callme target;
Thread t ;
public Caller(Callme targ , String s){
target = targ;
msg = s ;
t = new Thread(this);
t.start();

}
public void run(){
synchronized (target){
target.call(msg);
}
}
}
class Synch {
public static void main(String[] args) {
Callme target = new Callme();
Caller ob1 = new Caller(target,"Hello");
Caller ob2 = new Caller(target,"Synchronized");
Caller ob3 = new Caller(target,"World");
try {
ob1.t.join();
ob2.t.join();
ob3.t.join();
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
}
}


输出结果:

[Hello][Synchronized][world]

在这里,call()方法没有使用synchronized修饰,代替的是在Call的run()方法使用同步语句。

12、线程间通信

在前面的例子中,都是无条件的组织其他线程对相关方法的同步访问。这种监控器的对象的控制是很强大的,但是还可以通过线程内的通信进行更细微的通信来控制。

多线程通过把任务分成分散的逻辑单元来取代了事件循环,线程还提供了一个好处:它取消了循环环境检测。循环环境通常通过重复检查一个特定条件的循环,一旦条件为真,就采取相应的行动,它浪费了cpu时间。

为了避免循环检测,java通过wait()、notify()和notifyAll()方法实现了一个技巧的线程通信的机制。这些方法作为Object的final方法来实现,因此所有的类都包含他们,这三个方法只可以在一个同步的上下问访问,而从计算机科学角度上来说,它在概念上是很先进的,但是使用这些方法的规则很简单:

wait() 告诉线程先放弃监控器并进入睡眠状态,直到其他线程进入同一监控器并调用notify()方法

notify() 唤醒第一个等待同一对象的线程。

notifyAll()唤醒所有对同一对象调用wait()的线程,并且最高优先级的线程先运行。

这些方法在Object中声明,如下:

final void wait()throws InterruptedException

final void notify()

final void notifyAll()

wait()方法还有一种允许指定等待的时间的方式。

下面的程序错误地实现了生产者/消费者问题的简化形式。它包括了四个类:Q,试图同步的队列:Producer,产生队列输入的线程对象 ;Consumer,使用队列数据的

线程对象;PC,一个创建单个Q、Producer和Consumer的类。

class Q {
int n ;
synchronized int get(){
System.out.println("Got"+n);
return n;
}
synchronized void put(int n ){
this.n = n ;
System.out.println("Put: "+n);
}
}
class Producer implements Runnable {
Q q;
Producer(Q q){
this.q = q;
new Thread(this,"Producer").start();
}
public void run (){
int i = 0 ;
while (true){
q.put(i++);
}
}
class Consumer implements Runnable {
Q q;
Consumer(Q q){
this.q = q;
new Thread(this,"Consumer").start();
}
public void run(){
while(true){
q.get();
}

}
}
class PC {
public static void main(String[] args) {
Q q = new Q();
new Producer(q);
new Consumer(q);
System.out.println("Press Control-c to stop.");
}
}


13、暂停 恢复 和停止线程

有时暂停的执行是很有用的。例如,一个独立的线程用于显示白天的时间,如果用户不想要时间,可以暂停该线程。线程暂停以后,恢复也是简单的。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: