黑马程序员——多线程7:操作线程的其他方法

来源:互联网 发布:编程一小时官网 编辑:程序博客网 时间:2024/06/02 02:40

------- android培训、java培训、期待与您交流! ----------

1. 停止线程

1) 早期实现方式

        在Java的早期版本,如果需要停止某个线程运行,通常会使用Thread类中的stop方法。但是该方法因其固有的不安全性,在新版本中遭到删除,并不再被允许使用,如果编译器发现代码中包含该方法,就会提示该方法已过时。而之所以还可以在后续版本的API文档中看到stop方法是因为,早期的Java程序中使用到了该方法,因此为了便于后期程序员理解,还是将该方法添加到了API文档中。

2) 现在的实现方式

        相对于stop方法强制某个线程结束运行,停止线程最正常的方式其实就是结束run方法的执行,这也是目前停止线程的唯一方法。那么如何让run方法结束呢?通常子线程对象的run方法内都会定义循环结构,就像我们之前举过的所有例子一样,只要控制住循环,就可以让run方法结束,最终停止线程的运行。这就是停止线程的基本原理。我们结合下面的代码进行说明,

代码1:

class Demo implements Runnable{//标记private boolean flag = true;public void run(){while(flag){System.out.println(Thread.currentThread().getName()+"-----run");}}/*对外提供将标记置为假的方法一旦将标记置为假,run方法中的循环将会停止,线程也将停止运行*/public void setFlag(){flag = false;}}class StopThreadDemo{public static void main(String[] args){Demo d = new Demo(); Thread t1 = new Thread(d);Thread t2 = new Thread(d);t1.start();t2.start(); //定义循环次数,初值为0int count = 0;while(true){/*当主线程循环50次以后,将子线程标记置为假并通过break跳出循环,结束主线程随后子线程判断标记后也结束运行*/if(count == 50){d.setFlag();break;} System.out.println(Thread.currentThread().getName()+"------------"+count++);}}}
运行结果就是主线程循环执行50次以后,将子线程的标记置为假,并通过break,跳出循环结束主线程,随后子线程判断标记后也结束了run方法,停止线程运行。说明前述原理是可行的。

3) 例外

       我们设想这么一种错误情形:多个线程通过等待唤醒机制实现线程间通信,但是由于某种错误,导致所有线程均通过wait方法进入冻结状态,此时标记就不再起作用了,结果就是无法结束线程的运行。我们对代码1稍作修改,来模拟这种情况。

代码2:(这里只呈现修改部分代码)

public synchronized void run(){while(flag){//为了演示现象,故意把线程冻结try{wait();}catch(InterruptedException e){//这里我们进行了异常的非正式处理,后面会详细说明System.out.println(Thread.currentThread().getName()+"------------Exception");}System.out.println(Thread.currentThread().getName()+"-----run");}}
从运行结果来看,只有主线程正常运行,虽然主线程内将子线程run方法中的标记置为假,但是两个子线程由于处于冻结状态,无法判断标记,最终导致不能结束线程运行。

4) interrupt方法

       如果发生了上述代码2的情况该如何停止线程的运行呢?其实最简单的方式就是,“唤醒”冻结线程,使其判断已被置为假的标记即可,但是在所有线程均被冻结的情况下notify方法是不能使用的,这里我们就要介绍Thread类一个新的方法——interrupt。

       API文档中对该方法有这样的描述:如果线程在调用 Object 类的 wait()、wait(long) 或 wait(long, int) 方法,或者该类(Thread类)的 join()、join(long)、join(long, int)、sleep(long) 或 sleep(long, int) 方法过程中受阻,则其中断状态将被清除,它还将收到一个 InterruptedException。其中中断状态就是冻结状态,上述描述的意思是,如果某个线程处于冻结状态,那么调用interrupt方法就会强制清除该线程的冻结状态,促使其正常运行。但是调用该方法,会抛出InterruptedException异常。该方法的作用在于,只要在强制冻结线程和恢复正常运行前,将循环标记置为假,那么该线程判断标记后就可以结束运行了,就像如下代码,

代码3:

class Demo implements Runnable{private boolean flag = true;public synchronized void run(){while(flag){try{wait();}/*如果冻结的线程通过interrupt方法强制脱离冻结状态将抛出InterruptedException异常*/catch(InterruptedException e){System.out.println(Thread.currentThread().getName()+"------------Exception");/*抛出了InterruptedException异常表明希望通过interrupt方法,清除该线程的冻结状态并结束线程运行因此在此将标记置为假这就是异常的正式处理方式*/flag = false;                     }System.out.println(Thread.currentThread().getName()+"-----run");}} public void setFlag(){flag = false;}}class StopThreadDemo2{public static void main(String[] args){Demo d = new Demo(); Thread t1 = new Thread(d);Thread t2 = new Thread(d);t1.start();t2.start(); int count = 0;while(true){if(count == 50){/*调用两个线程的interrupt方法强制线程脱离冻结状态,令其继续运行,判断标记此时标记已在catch代码块中被置为假结束循环*/t1.interrupt();t2.interrupt();break;} System.out.println(Thread.currentThread().getName()+"------------"+count++);}}}
该代码运行结果的最后部分如下:

main------------47

main------------48

main------------49

Thread-1------------Exception

Thread-1-----run

Thread-0------------Exception

Thread-0-----run

       结果表明,主线程循环执行50次以后(此时两个子线程早已进入冻结状态),清除两子线程的冻结状态时确实抛出了异常(打印了两个Exception),并且两线程得以正常运行后(两个线程的run输出),经标记判断结束了线程的运行。

       那么从上述代码我们可以看出,interrupt方法就是用于异常处理,它可以避免因某些错误导致所有线程处于冻结状态时,无法结束程序的问题。

2. 守护线程

       Thread类中有一个名为setDaemon的方法,API文档中的描述是这样的:将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。 该方法必须在启动线程前调用。我们通过下面的代码来观察该方法的作用,

代码4:

class Demo implements Runnable{public voidrun(){//故意定义为无线循环while(true){System.out.println(Thread.currentThread().getName()+"----------run");}}}class DaemonDemo{public staticvoid main(String[] args){Demo d= new Demo(); Thread t1 = new Thread(d);Thread t2 = new Thread(d); //在启动线程以前将两线程定义为守护线程t1.setDaemon(true);t2.setDaemon(true); t1.start();t2.start(); //定义主线程的循环为有限循环for(int x = 0; x<10; x++)System.out.println(Thread.currentThread().getName()+"-----"+x);}}
从代码4的运行结果来看,主线程结束运行以后,整个程序也停止了运行。上述例子中,在主线程结束运行以后,只有两个子线程是活动线程(由无线循环保证其活动),并且都是守护线程,在这种情况下Java虚拟机就会退出,两子线程的运行也会强制停止。当然,即使两个线程处于冻结状态,也会强制结束的。守护线程在启动和运行时与一般线程是没有区别的,但守护线程的结束受到其他活动线程的制约——随着其他线程的结束而结束。

        在线程B的运行依赖于线程A的情况下,比如缓存的写入和读取,写入线程不再写入数据时,读取线程也就没有存在的意义了,此时就可以将读取线程设置为守护线程。

        最后再强调一点:一定要在线程启动前将线程设置为守护线程。

3. Join方法

我们通过下面的代码来说明join方法的具体作用,

代码5:

class Demo implements Runnable{public void run(){for(int x = 0; x<50; x++){System.out.println(Thread.currentThread().getName()+"------------"+x);}}}class JoinDemo{public staticvoid main(String[] args){Demo d= new Demo(); Thread t1 = new Thread(d);Thread t2 = new Thread(d);t1.start(); //调用t1线程的join方法try{t1.join();}catch(InterruptedExceptione){} t2.start(); for(int x= 0 ; x<50; x++)System.out.println(Thread.currentThread().getName()+"---"+x);}}
不论运行多少次代码5,t1线程(控制台显示为Thread-0)总会首先运行,并且只有等到其运行完毕,主线程和t2线程才能陆续开始运行。

       那么join方法的真正含义是什么呢?一旦调用了线程对象的join方法,该线程就会强制获取到CPU的执行权。也就是说主线程执行完“try{t1.join();}catch(InterruptedException e){}”语句以后,就会释放CPU执行权,并且与此同时,主线程就会进入冻结状态(与sleep和wait方法类似),不仅失去了执行权,也失去了执行资格。此时由于没有其他活动线程(t2线程此时还未启动),因此t1线程得以“毫无干扰”地运行完毕。t1运行结束后,主线程就会自动唤醒,继续向下执行,启动t2线程,并与之交替打印。

我们再回过头来查看join方法的API文档,我们就可以理解该方法的描述了:等待该线程终止。意思就是说,一直等到调用join方法的线程运行完毕,再开始执行下面的代码。

如果将t1.join()语句放到t2.start()语句后执行将会是什么结果呢?结果就是,t1与t2交替打印以后,主线程再继续打印,这里主线程的等待与唤醒与t2是没有关系的,还是依赖于t1线程的运行。

        综上所述,join方法的特点就是:当A线程执行到B线程的join方法时,A就会等待,直到B执行完毕,才会唤醒继续执行。该方法的应用场景就是,当满足某个条件时临时加入一个子线程,令其运行完毕以后,主线程继续运行。

 

小知识点1:

        Java标准类库中每个类都会继承或者复写根父类Object的toString方法,Thread类也不例外。查阅Thread的API文档,对其toString方法的描述是:返回该线程的字符串表示形式,包括线程名称、优先级和线程组。这里提到了一个线程组的概念。标准类库java.lang包内的ThreadGroup类就是用于描述线程组的类,对该类的描述为:线程组表示一个线程的集合。以主线程为例,在主函数内创建并开启的子线程都属于主线程组,换句话说,由A线程创建并开启的所有子线程都属于A线程组的。不过实际开发中,对线程组的应用较少,这里仅做一个简单介绍。

 

4. 设置线程的优先级

        上述小知识点中提到Thread类toString方法除了返回线程对象的名称和线程组以外,还会返回该线程对象的优先级。我们可以把优先级简单理解CPU操作某个线程的频率,或者CPU处理某个线程的倾向。当然,某个线程的优先级越高,CPU处理的频率就相对越高,越倾向于优先处理那个线程。Thread类中setPriority方法就是用于设置线程的优先级(包括主线程)。优先级数值从1到10分为十个等级,数字越大优先级越高,所有线程的默认优先级为5。但是小幅度调整优先级数值,效果并不明显,因此如果确实需要优先处理某个线程,建议设置优先级为10,反之设置为1,如果没有特别需求,就设为5即可。

        为了提高代码的阅读性,对上述常用的优先级等级进行了封装并对外提供为了全局常量——MAX_PRIORITY,MIN_PRORITY以及MAX_PRORITY,分别对应10、1和5优先级。不过实际测试效果来看,上述三个优先级的效果还是不够明显,尤其程序运行此时较少的情况下。有兴趣的朋友,可以动手测试一下。

5. yield方法

        API文档中对该方法的描述为:暂停当前正在执行的线程对象,并执行其他线程。我们通过下面的代码来观察该方法的实际效果,

代码6:

class Test implements Runnable{public void run(){for(int x = 0; x<50; x++)System.out.println(Thread.currentThread()+"---"+x);             //每次循环都调用一次yield方法Thread.yield();}}class YieldDemo{public staticvoid main(String[] args){Test t= new Test(); Thread t1 = new Thread(t);Thread t2 = new Thread(t); t1.start();t2.start();}}
截取部分运行结果如下:

Thread[Thread-0,5,main]---0

Thread[Thread-1,5,main]---0

Thread[Thread-0,5,main]---1

Thread[Thread-1,5,main]---1

Thread[Thread-0,5,main]---2

Thread[Thread-1,5,main]---2

Thread[Thread-0,5,main]---3

Thread[Thread-1,5,main]---3

Thread[Thread-0,5,main]---4

Thread[Thread-1,5,main]---4

Thread[Thread-0,5,main]---5

Thread[Thread-1,5,main]---5

        其运行特点就是两个子线程在不断交替运行。yield方法的实际作用其实是暂时性的强制当前线程放弃执行权,那么t1打印一个数字,放弃执行权,t2获取执行权,打印一个数字,放弃执行权,t1又获得执行权,打印一个数字,以此类推,就出现了两个线程交替运行的现象。那么调用该方法的目的在于,尽可能让CPU更为平均的处理每个线程,避免由于某个线程长期持有执行权,而造成其他线程无法运行的现象。

0 0