问题
在第4.1节中,已经提到如果不对程序中的两个线程进行合理地调度,则会输出不可预测的结果,因此就要求必须采用一些特殊的方法来有效地控制线程。但是,利用线程优先级显然是一种不可靠的方案,那么如何既保证资源的有效利用,又能保证程序的结果能够按照预期的那样被正确地输出呢?
解决思路
多线程的引入往往使程序显示出无法预料的结果,这是因为线程和线程之间在获得CPU时间片的机会上没有一定的规律。通常情况下当多线程创建以后,都不会任其执行,而是为达到特定目的有效地调度这些线程。Java提供了一系列控制线程的方法和手段,使之能在充分利用系统资源的前提下,还能够保证程序结果按照要求被正确地输出和显示。这些控制的手段有:等待一个线程结束、合并线程、设置守护线程以及中断一个线程等。下面就一一介绍这些控制线程的手段。
为了能够更好地控制线程,实现线程的合理调度,首先需要了解一下线程的生命周期。一个线程从创建到消亡存在四种状态,分别介绍如下。
1 / 创建状态(new Thread)
当一个线程被实例化后,它就处于创建状态,直到调用 start()方法。当一个线程处于创建状态时,它仅仅是一个空的线程对象,系统不为它分配资源。在这个状态下,线程还不是活(alive)的。
要想使线程处于创建状态,可以通过执行下面的语句来实现:
|
2 / 可运行状态(Runnable)
对于处于创建状态的线程来说,只要对其调用了start()方法,就可以使该线程进入可运行状态。当一个线程处于可运行状态时,系统为这个线程分配它所需要的系统资源,然后安排其运行并调用线程的run()方法。此时的线程具备了所有能够运行的条件,但是,此状态的线程并不一定马上就被执行。这是因为目前所使用的计算机大多都是单处理器的,因此,要想在同一时刻运行所有的处于可运行状态的线程是不可能的。这时还需要Java的运行系统(JRE)通过合理的调度策略才能使此状态下的线程真正得到CPU。不过线程在这种状态下已经被视为是活(alive)的了。
要想使线程处于可运行状态,可以通过执行下面的语句来实现:
|
3 / 不可运行状态(Not Runnable)
可以认为,在下面列出的条件之一发生时,线程进入不可运行状态。
◆调用了sleep()方法;
◆为等候一个条件变量,线程调用了wait()方法;
◆输入输出流中线程被阻塞。
不可运行状态也可以称为阻塞状态(Blocked)。导致线程阻塞的原因有很多,比如等待消息,输入输出不合理等。此时,即使处理器空闲,系统也无法执行这些处于阻塞状态的线程,除非阻塞条件被破坏,如:sleep的时间到达、得到条件变量之后使用了notify()方法或者 notifyAll()方法唤醒了waiting中的一个或所有线程、获得了需要的I/O资源等。这些方法会在后面进行介绍。在这种状态下,线程也被视为是alive的。
4 / 消亡状态(Dead)
一般来讲,线程的run()方法执行完毕时,该线程就被视为进入了消亡状态。一个处于消亡状态的线程不能再进入其他状态,即使对它调用 start()方法也无济于事。可以通过两种方法使线程进入消亡状态:自然撤消(线程执行完)或是强行中止(比如,调用了Thread类的stop()方法)。需要说明的是,stop()方法属于Java的早期版本,现在已作为保留的方法不再使用,因此要终止一个线程,需要通过其他方法来实现,如interrupt()。这一方法将在第4.4节中介绍。
下面的图4.3.1描绘出了线程从创建到消亡的整个过程,读者可以结合前面分析的每个阶段来把握线程的生命周期。
了解线程的生命周期对更好地控制线程是很有帮助的,因为可以控制每一个线程使其处于指定的状态,从而完成特定的任务。但是也会因为方法使用的不当而使线程表现出更加难以预料的结果,比如本想使一个线程休眠等待,却将其结束等。这就要求不仅要对线程的生命周期有很好的理解,而且还能够调用正确的方法来控制线程。这一节就介绍一种控制线程的方法:如何等待一个线程结束。
图4.3.1 线程的生命周期 |
具体步骤
在现实问题域中,有可能需要等待一个线程执行结束后再运行另一个线程,这时可以利用Java提供的两种方法来实现这个功能。
(1)调用线程类的isAlive()方法和sleep()方法来等待某个或某些线程结束
下面这个例子将创建两个线程,并分别依次输出两个直角三角形。
|
题目要求先运行第一个线程t1,输出一个直角三角形,然后等待第一个线程终止后再运行第二个线程t2,再输出一个直角三角形。而实际运行的结果如何呢?编译并运行这个程序,就会发现,实际运行的结果并不是两个直角三角形,而是一些乱七八糟的“*”号行,有的长,有的短。为什么会这样呢?很显然,因为线程并没有按照预期的调用顺序来执行,而是产生了线程赛跑的现象。实际上前面已经多次提到,Java在不进行任何控制的情况下不可能按照调用顺序来执行线程,而是交替执行。如果要想得到预期的结果,就需要对这两个线程加以适当的控制,让第一个线程先执行,并判断第一个线程是否已经终止,如果已经终止,再调用第二个线程来执行。
解决这个问题的方案是通过调用Thread类的isAlive()方法来实现题目要求。它的语法格式如下所示:
|
该方法可以用来测试当前线程是否仍处于活动状态,如果处于活动状态,则返回true,保持该线程继续运行,否则返回false,结束该线程并做其他处理。
对例4.3.1的代码做一些改动,在主类中引入isAlive()方法,改动后的代码如下所示:
|
本程序中,先创建了两个线程t1和t2,使其进入创建状态,然后启动线程t1,使其进入可运行状态,通过调度使其得到CPU从而先被执行。如果线程t1未执行结束,则使用sleep()方法让主线程休眠,使其进入不可运行状态,一直等待线程t1执行结束。一旦t1执行结束,该线程就进入消亡态,此时线程t1的状态从alive变成dead。循环测试结束,主线程从不可运行态回到可运行态,从而得到CPU的控制权,执行t2.start()方法,启动t2线程。接着主线程执行完毕,进入消亡态,而线程t2响应start()方法进入可运行态,根据调度策略获得CPU控制权,输出第二个直角三角形。最后线程t2自然结束,进入消亡态,整个程序结束。
执行这个程序,结果依次输出了两个直角三角形,这说明isAlive()方法和循环语句配合使用可以通过判断一个线程是否结束,实现使其他线程等待的目的。
(2)调用线程类的join()方法来等待某个或某些线程结束
Thread类中的join()方法也可以用来等待一个线程的结束,而且这个方法更为常用,它的语法格式如下所示:
|
该方法将使得当前线程等待调用该方法的线程结束后, 再恢复执行。由于该方法被调用时可能抛出一个InterruptedException异常,因此在调用它的时候需要将它放在try…catch语句中。对前面的程序做一些改动,引入join()方法,并观察程序运行的结果是否有所变化。改动后的代码如下所示:
|
编译并运行程序,可以看到,输出的结果和例4.3.2完全相同,也是依次输出了两个直角三角形。这是怎么实现的呢?在try…catch语句中,程序调用了t1.join()方法,该方法将使t1这个线程合并到调用t1.join();语句的线程中,在该程序中这个线程就是主线程。一旦t1合并到主线程以后,程序就一直执行线程t1,而主线程进入不可运行态,并一直这样等待,直到线程t1执行结束为止,主线程恢复,进入可运行态,程序才执行t2.start(),启动线程t2。此外也可以看出,使用join()方法来等待一个线程比使用isAlive()方法更加简洁。
通过查看JDK帮助文档,还可以看到,在Thread类中,除了一个无参的join()方法之外,还有两个带参的join()方法,它们分别是join(long millis)和join(long millis,int nanos)。使用它们不仅可以合并线程,而且还指定了合并的时间,前者精确到毫秒,后者精确到纳秒。当合并时间到达时,两个合并的线程会恢复到合并前的状态。当然,使用这两个方法的时候仍需要将它们放在try…catch语句中以处理可能发生的异常。
这三个join()方法其实很像是将while循环、isAlive()方法以及sleep()方法在功能上进行的组合,只不过join()方法将它们给封装了起来。因此,使用该方法等待一个线程结束将使程序更加简练,值得提倡。
专家说明
通过前面的学习,掌握了如何实现等待一个线程结束的方法。其中,一种方法是通过while循环使用isAlive()方法和sleep()方法配合来实现,一种方法是通过在程序中调用join()方法来实现。这两种方法表现的结果是相同的,读者可以根据实际情况任选其一。但是也应该看到,join()方法在解决这类问题时更有优势。
专家指点
在本节中,通过对线程生命周期的了解将对线程的执行过程中所处的每一个状态有深刻的认识,这就保证了编写多线程的Java程序时,能够更好地了解程序中每个线程在运行当中可能处于的状态,从而很好地控制线程。其实,Java的Thread类中提供了很多能够控制线程的方法。这些方法会在本章后面的小节中以问题的形式一一介绍。读者一定要善于利用这些方法,使程序不仅能够被执行,而且能够被正确实现。
此外,通过本节中所给出的例子还发现,程序中即使主线程结束,如果一般线程还 未结束,程序仍然不会结束,这显然是不安全的。那么怎么样才能保证当主线程结束时,所有其他线程无论执行到什么状态都必须随之一起结束呢?在相关问题中将回答这一问题。
相关问题
在Java程序当中,可以把线程分为两类:用户线程和守护线程(又称为后台线程)。用户线程是那些完成有用工作的线程,也就是前面所说的一般线程。守护线程是那些仅提供辅助功能的线程。这类线程可以监视其他线程的运行情况,也可以处理一些相对不太紧急的任务。在一些特定的场合,经常会通过设置守护线程的方式来配合其他线程一起完成特定的功能。
Thread 类提供了setDaemon()方法用来打开或者关闭一个线程的守护状态(Daemon)。通过Thread类提供的isDaemon()方法,还可查看一个线程是不是一个处于守护状态的线程。如果是一个Daemon线程,那么它创建的任何线程也会自动具备Daemon属性。
下面先来看一个简单的程序演示Daemon线程的用法:
|
运行程序,从键盘输入一个字符串yes或者Y的时候,程序将创建一个守护线程。紧接着主线程执行结束,守护线程也随之消亡,此时在线程的run()方法中循环语句刚开始执行就结束了,这就说明守护线程随用户线程结束而一起结束,无论其执行是否完毕。如果从键盘输入一个字符串no或者N的时候,程序将创建一个用户线程。这样,不管主线程是否结束,该用户线程都要执行循环100次,而且通过输出的线程状态是:Daemon is false,也说明了该线程不是守护线程,可在主线程结束之后继续运行直到run()方法执行结束为止。
该程序中无论创建用户线程还是创建守护线程,都只有两个线程在运行,那么如果有更多的线程在执行呢?下面再举一个例子来演示Daemon线程的用法:该例子源于《Java编程思想》一书,中间有一些代码做了改动,并添加了注释,以供读者参考。
|
程序中,Daemon线程先将自己的Daemon标记设置成“真”,然后产生一系列线程,因而这些线程也具有Daemon属性。一旦main()方法完成自己的工作,便没有什么能阻止程序中断运行,因为这里运行的只有Daemon线程。所以能看到启动所有Daemon线程后显示出来的结果,System.in也进行了相应的设置,使程序中断前能等待一个回车。如果不进行这样的设置,就只能看到创建Daemon线程的一部分结果。
将readLine()代码换成不同长度的sleep()调用,catch子句的参数声明为Interrupted- Exception异常,看看会有什么表现。结果显示守护线程还没有创建完成,整个程序就结束了,从这一点也说明了守护线程不是程序的基本部分,当非Daemon线程全部结束以后,程序就会中止运行,而不管是否还有Daemon线程的存在。
“Daemon”线程的作用就是在程序的运行期间于后台提供一种“常规”服务,但它并不属于程序的一个基本部分。在 Java 虚拟机 (JVM) 中,即使在 main 线程结束以后,如果另一个用户线程仍在运行,则程序仍然可以继续运行。Java 程序将运行到所有用户线程终止,然后它将破坏所有的守护线程。因此,一旦所有非Daemon线程完成,程序也会中止运行。
回书目 上一节 下一节 |