问题
Java中提供了很多调度线程的方法,上一节介绍了其中一种控制线程的方法:如何等待一个线程结束。那么如果不希望等待线程结束,而是根据问题的需要随时都要中断线程使其结束,这种对线程的控制方法该如何实现呢?
解决思路
首先必须先明确“中断”这个概念的实际含义,这里的中断是指一个线程在其任务完成之前被强行停止,提前消亡的过程。查阅JDK的帮助文档,可以找到这样一个和中断有关的方法:interrupt()。
它的语法格式如下所示:
该方法的功能是中断一个线程的执行。但是,在实际使用当中发现,这个方法不一定能够真地中断一个正在运行的线程。下面通过一个例子来看一看使用interrput()方法中断一个线程时所出现的结果。程序代码如下所示:
// 例4.4.1 InterruptThreadDemo.java
class MyThread extends Thread
{
public void run()
{
while(true) // 无限循环,并使线程每隔1秒输出一次字符串
{
System.out.println(getName()+" is running");
try{
sleep(1000);
}catch(InterruptedException e){
System.out.println(e.getMessage());
}
}
}
}
class InterruptThreadDemo
{
public static void main(String[] args) throws InterruptedException
{
MyThread m=new MyThread(); // 创建线程对象m
System.out.println("Starting thread...");
m.start(); // 启动线程m
Thread.sleep(2000); //主线程休眠2秒,使线程m一直得到执行
System.out.println("Interrupt thread...");
m.interrupt(); // 调用interrupt()方法中断线程m
Thread.sleep(2000); // 主线程休眠2秒,观察中断后的结果
System.out.println("Stopping application..."); // 主线程结束
}
}
这个程序的本意是希望,当程序执行到m.interrupt()方法后,线程m将被中断并进入消亡状态。然而运行这个程序,屏幕里显示了出人意料的结果,如图4.4.1所示。
通过对结果的分析,可以发现,用户线程在调用了interrupt()方法之后并没有被中断,而是继续执行,直到人为地按下Ctrl+C或者Pause键为止。这个例子说明一个事实,直接使用interrput()方法并不能中断一个正在运行的线程。那么用什么样的方法才能中断一个正在运行的线程呢?
图 4.4.1 对线程调用了interrupt()
通过查阅JDK,有些读者可能会看到Thread类中所提供的stop()方法。但是在这里需要强调的是,虽然该方法确实能够停止一个正在运行的线程,但是该方法是不安全的,因为有时使用它会导致严重的系统错误。例如一个线程正在等待关键的数据结构,并只完成了部分地改变,如果在这一时刻停止该线程,那么数据结构将会停留在错误的状态上。正因为如此,在Java后期的版本中,它将不复存在。因此,使用stop()方法来中断一个线程是不合适的。
这时我们想到了使用共享变量的方式,通过一个共享信号变量来通知线程是否需要中断,如果需要中断,则停止正在执行的任务,否则让任务继续执行。这种方式是如何实现的呢?
具体步骤
在这种方式中,之所以引入共享变量,是因为该变量可以被多个执行相同任务的线程用来作为是否中断的信号,通知中断线程的执行。下面通过在程序中引入共享变量来改进前面例4.4.1,改进后的代码如下所示:
// 例4.4.2 InterruptThreadDemo2.java
class MyThread extends Thread
{
boolean stop = false; // 引入一个布尔型的共享变量stop
public void run()
{
while(!stop) // 通过判断stop变量的值来确定是否继续执行线程体
{
System.out.println(getName()+" is running");
try
{
sleep(1000);
}catch(InterruptedException e){
System.out.println(e.getMessage());
}
}
System.out.println("Thread is exiting...");
}
}
class InterruptThreadDemo2
{
public static void main(String[] args) throws InterruptedException
{
MyThread m=new MyThread();
System.out.println("Starting thread...");
m.start();
Thread.sleep(3000);
System.out.println("Interrupt thread...");
m.stop=true; // 修改共享变量
Thread.sleep(3000); // 主线程休眠以观察线程中断后的情况
System.out.println("Stopping application...");
}
}
在使用共享变量来中断一个线程的过程中,线程体通过循环来周期性的检查这一变量的状态。如果变量的状态改变,说明程序发出了立即中断该线程的请求,此时,循环体条件不再满足,结束循环,进而结束线程的任务。程序执行的结果如图4.4.2所示:
图4.4.2 引入共享变量来中断线程
其中,主程序中的第二个Thread.sleep(3000);语句就是用来使程序不提早结束,以便观察线程m的中断情况。结果是一旦将共享变量stop设置为true,则中断立即发生。
为了更加安全起见,通常需要将共享变量定义为volatile类型或者将对该共享变量的一切访问封装到同步的代码或者同步方法中去。后者所提到的技术将在第4.5节中介绍。
在多线程的程序中,当出现有两个或多个线程共享同一实例变量的情况时,每一个线程可以保持这个实例变量自己的私有副本,变量的实际备份在不同时间被更新。而问题就是变量的主备份总是需要反映它的当前状态,此时反而使效率降低。为保证效率,只需要简单地指定变量为volatile类型即可,它可以告诉编译器必须总是使用volatile变量的主备份(或者至少总是保持任何私有的备份和最新的备份一样,反之亦然)。同样,对主变量的访问必须同任何私有备份一样,精确地顺序执行。
如果需要一次中断所有由同一线程类创建的线程,该怎样实现呢?有些读者可能马上就想到了对每一个线程对象通过设置共享变量的方式来中断线程。这种方法当然可以,那么有没有更好的方法呢?
此时只需将共享变量设置为static类型的即可。然后在主程序中当需要中断所有同一个线程类创建的线程对象时,使用MyThread.stop=true;语句就可实现对所有同一个线程类创建的线程对象的中断操作,而且效率明显提高。读者不妨试一试。
专家说明
通过本节介绍了如何中断一个正在执行的线程,既不是用stop()方法,也不是用interrupt()方法,而是通过引入了共享变量的形式有效地解决了线程中断的问题。其实这种方法有很多好处,它避免了一些无法想象的意外情况的发生,特别是将共享变量所访问的一切代码都封装到同步方法中以后,安全性将更高。在本节中,还可以尝试创建多个线程来检验这种中断方式的好处。此外,还介绍了volatile类型说明符的作用,这更加有助于提高中断线程的效率,值得提倡。
专家指点
本小节不仅要掌握如何使用共享变量的方法来中断一个线程,还要明白为什么使用其他方法来中断线程就不安全。其实,在多线程的调度当中还会出现一个问题,那就是死锁。死锁的出现将导致线程间均无法向前推进,从而陷入尴尬的局面。因此,为减少出现死锁的发生,Java 1.2以后的版本中已经不再使用Thread类的stop(),suspend(),resume()以及destroy()方法。特别是不安全的stop()方法,原因就是它会解除由线程获取的所有锁定,而且一旦对象处于一种不连贯的状态,那么其他线程就能在那种状态下检查和修改它们,结果导致很难再检查出问题的真正所在。因此最好的方法就是,用一个标志来告诉线程什么时候应该退出自己的run()方法,并中断自己的执行。通过后面小节的学习将会更好的理解这个问题。
相关问题
如果一个线程由于等待某些事件的发生而被阻塞,又该如何实现该线程的中断呢?比如当一个线程由于需要等候键盘输入而被阻塞,处于不可运行状态时,即使主程序中将该线程的共享变量设置为true,但该线程此时根本无法检查循环标志,当然也就无法立即中断。
其实,这种情况经常会发生,比如调用Thread.join()方法,或者Thread.sleep()方法,在网络中调用ServerSocket.accept()方法,或者调用了DatagramSocket.receive()方法时,都有可能导致线程阻塞。即便这样,仍然不要使用stop()方法,而是使用Thread提供的interrupt()方法,因为该方法虽然不会中断一个正在运行的线程,但是它可以使一个被阻塞的线程抛出一个中断异常,从而使线程提前结束阻塞状态,退出堵塞代码。
下面看一个例子来说明这个问题:
// 例4.4.3 InterruptThreadDemo3.java
class MyThread extends Thread
{
volatile boolean stop = false;
public void run()
{
while(!stop)
{
System.out.println(getName()+" is running");
try
{
sleep(1000);
}catch(InterruptedException e){
System.out.println("week up from blcok...");
stop=true; // 在异常处理代码中修改共享变量的状态
}
}
System.out.println(getName()+" is exiting...");
}
}
class InterruptThreadDemo3
{
public static void main(String[] args) throws InterruptedException
{
MyThread m1=new MyThread();
System.out.println("Starting thread...");
m1.start();
Thread.sleep(3000);
System.out.println("Interrupt thread...:"+m1.getName());
m1.stop=true; // 设置共享变量为true
m1.interrupt(); // 阻塞时退出阻塞状态
Thread.sleep(3000); // 主线程休眠3秒以便观察线程m1的中断情况
System.out.println("Stopping application...");
}
}
程序中如果线程m1发生了阻塞,那么虽然执行了m1.stop=true;语句,但是stop的值并未改变。为了能够中断该线程,必须在异常处理语句中对共享变量的值进行重新设置,从而实现了在任何情况下都能够中断线程的目的。
一定要记住,m1.interrupt();语句只有当线程发生阻塞时才有效。它的作用就是抛出一个InterruptedException类的异常对象,使try…catch语句捕获异常,并对其进行处理。请读者仔细研究这个程序,以便能够看出其中的巧妙之处。