Thread之十:停止线程方法汇总

在上篇文章《多线程的使用——Thread类和Runnable接口》中提到中断线程的问题。在JAVA中,曾经使用stop方法来停止线程,然而,该方法具有固有的不安全性,因而已经被抛弃(Deprecated)。那么应该怎么结束一个进程呢?官方文档中对此有详细说明:《为何不赞成使用 Thread.stop、Thread.suspend 和 Thread.resume?》。在此引用stop方法的说明:

1. Why is Thread.stop deprecated?
Because it is inherently unsafe. Stopping a thread causes it to unlock all the monitors that it has locked. (The monitors are unlocked as the ThreadDeath exception propagates up the stack.) If any of the objects previously protected by these monitors were in an inconsistent state, other threads may now view these objects in an inconsistent state. Such objects are said to be damaged. When threads operate on damaged objects, arbitrary behavior can result. This behavior may be subtle and difficult to detect, or it may be pronounced. Unlike other unchecked exceptions, ThreadDeath kills threads silently; thus, the user has no warning that his program may be corrupted. The corruption can manifest itself at any time after the actual damage occurs, even hours or days in the future.
大概意思是:
因为该方法本质上是不安全的。停止一个线程将释放它已经锁定的所有监视器(作为沿堆栈向上传播的未检查 ThreadDeath 异常的一个自然后果)。如果以前受这些监视器保护的任何对象都处于一种不一致的状态,则损坏的对象将对其他线程可见,这有可能导致任意的行为。此行为可能是微妙的,难以察觉,也可能是显著的。不像其他的未检查异常,ThreadDeath异常会在后台杀死线程,因此,用户并不会得到警告,提示他的程序可能已损坏。这种损坏有可能在实际破坏发生之后的任何时间表现出来,也有可能在多小时甚至在未来的很多天后。
在文档中还提到,程序员不能通过捕获ThreadDeath异常来修复已破坏的对象。具体原因见原文。
既然stop方法不建议使用,那么应该用什么方法来代理stop已实现相应的功能呢?
 
1、通过修改共享变量来通知目标线程停止运行
 
大部分需要使用stop的地方应该使用这种方法来达到中断线程的目的。
这种方法有几个要求或注意事项:
(1)目标线程必须有规律的检查变量,当该变量指示它应该停止运行时,该线程应该按一定的顺序从它执行的方法中返回。
(2)该变量必须定义为volatile或者所有对它的访问必须同步(synchronized)。
例如:
package chapter2; 

public class ThreadFlag extends Thread 
{ 
    public volatile boolean exit = false; 

    public void run() 
    { 
        while (!exit); 
    } 
    public static void main(String[] args) throws Exception 
    { 
        ThreadFlag thread = new ThreadFlag(); 
        thread.start(); 
        sleep(5000); // 主线程延迟5秒 
        thread.exit = true;  // 终止线程thread 
        thread.join(); 
        System.out.println("线程退出!"); 
    } 
} 

 在上面代码中定义了一个退出标志exit,当exit为true时,while循环退出,exit的默认值为false.在定义exit时,使用了一个Java关键字volatile,这个关键字的目的是使exit同步,也就是说在同一时刻只能由一个线程来修改exit的值。

2、通过Thread.interrupt方法中断线程
通常情况下,我们应该使用第一种方式来代替Thread.stop方法。然而以下几种方式应该使用Thread.interrupt方法来中断线程(该方法通常也会结合第一种方法使用)。
一开始使用interrupt方法时,会有莫名奇妙的感觉:难道该方法有问题?
API文档上说,该方法用于"Interrupts this thread"。请看下面的例子:
package com.jvm.study.thread;

public class TestThread implements Runnable{ 
     
    boolean stop = false; 
    public static void main(String[] args) throws Exception { 
        Thread thread = new Thread(new TestThread(),"My Thread"); 
        System.out.println( "Starting thread..." ); 
        thread.start(); 
        Thread.sleep( 3000 ); 
        System.out.println( "Interrupting thread..." ); 
        thread.interrupt(); 
        System.out.println("线程是否中断:" + thread.isInterrupted()); 
        Thread.sleep( 3000 ); 
        System.out.println("Stopping application..." ); 
    } 
    public void run() { 
        while(!stop){ 
            System.out.println( "My Thread is running..." ); 
            // 让该循环持续一段时间,使上面的话打印次数少点 
            long time = System.currentTimeMillis(); 
            while((System.currentTimeMillis()-time < 1000)) { 
            } 
        } 
        System.out.println("My Thread exiting under request..." ); 
    } 
} 
运行后的结果是:
Starting thread...
My Thread is running...
My Thread is running...
My Thread is running...
My Thread is running...
Interrupting thread...
线程是否中断:true
My Thread is running...
My Thread is running...
My Thread is running...
Stopping application...
My Thread is running...
My Thread is running...
……
应用程序并不会退出,启动的线程没有因为调用interrupt而终止,可是从调用isInterrupted方法返回的结果可以清楚地知道该线程已经中断了。那位什么会出现这种情况呢?到底是interrupt方法出问题了还是isInterrupted方法出问题了?在Thread类中还有一个测试中断状态的方法(静态的)interrupted,换用这个方法测试,得到的结果是一样的。由此似乎应该是interrupt方法出问题了。实际上,在JAVA API文档中对该方法进行了详细的说明。该方法实际上只是设置了一个中断状态,当该线程由于下列原因而受阻时,这个中断状态就能起作用了
(1)如果线程在调用 Object 类的 wait()、wait(long) 或 wait(long, int) 方法,或者该类的 join()、join(long)、join(long, int)、sleep(long) 或 sleep(long, int) 方法过程中受阻,则其中断状态将被清除,它还将收到一个InterruptedException异常。这个时候,我们可以通过捕获InterruptedException异常来终止线程的执行,具体可以通过return等退出或改变共享变量的值使其退出。
(2)如果该线程在可中断的通道上的 I/O 操作中受阻,则该通道将被关闭,该线程的中断状态将被设置并且该线程将收到一个 ClosedByInterruptException。这时候处理方法一样,只是捕获的异常不一样而已。
其实对于这些情况有一个通用的处理方法:
package com.jvm.study.thread;

public class TestThread2 implements Runnable {

    boolean stop = false;

    public static void main(String[] args) throws Exception {
        Thread thread = new Thread(new TestThread2(), "My Thread2");
        System.out.println("Starting thread...");
        thread.start();
        Thread.sleep(10000);
        System.out.println("Interrupting thread...");
        thread.interrupt();
        System.out.println("线程是否中断:" + thread.isInterrupted());
        Thread.sleep(3000);
        System.out.println("Stopping application...");
        Thread.sleep(30000);
    }

    public void run() {
        while (!stop) {
            System.out.println("My Thread is running...");
            // 让该循环持续一段时间,使上面的话打印次数少点
            long time = System.currentTimeMillis();
            while ((System.currentTimeMillis() - time < 1000)) {
            }
            if (Thread.currentThread().isInterrupted()) {
                return;
            }
        }
        System.out.println("My Thread exiting under request...");
    }
}
因为调用interrupt方法后,会设置线程的中断状态,所以,通过监视该状态来达到终止线程的目的。这些在《Thread之八:interrupt中断》也提到过。
 
总结:程序应该对线程中断作出恰当的响应。响应方式通常有三种:(来自温绍锦(昵称:温少):http//www.cnblogs.com/jobs/)

注意:interrupted与isInterrupted方法的区别(见API文档)

 

 当线程处于下面的Blocked状态,通过调用interrupt()方法来停止线程:

2.1、当线程处于下面的状况时(1)如果线程在调用 Object 类的 wait()、wait(long) 或 wait(long, int) 方法,或者该类的 join()、join(long)、join(long, int)、sleep(long) 或 sleep(long, int) 方法过程中受阻,则其中断状态将被清除,它还将收到一个InterruptedException异常。这个时候,我们可以通过捕获InterruptedException异常来终止线程的执行,具体可以通过return等退出或改变共享变量的值使其退出。),属于非运行状态:

  • 当sleep方法被调用。

  • 当wait方法被调用。

  • 当被I/O阻塞,可能是文件或者网络等等。

当线程处于上述的状态时,使用前面介绍的方法就不可用了。这个时候,我们可以使用interrupt()来打破阻塞的情况,如:

public void stop() {
        Thread tmpBlinker = blinker;
        blinker = null;
        if (tmpBlinker != null) {
           tmpBlinker.interrupt();
        }
    }

 

interrupt()被调用的时候,InterruptedException将被抛出,所以你可以再run方法中捕获这个异常,让线程安全退出:

try {
   ....
   wait();
} catch (InterruptedException iex) {
   throw new RuntimeException("Interrupted",iex);
}

 

2.2、阻塞的I/O((2)如果该线程在可中断的通道上的 I/O 操作中受阻,则该通道将被关闭,该线程的中断状态将被设置并且该线程将收到一个 ClosedByInterruptException。这时候处理方法一样,只是捕获的异常不一样而已

当线程被I/O阻塞的时候,调用interrupt()的情况是依赖与实际运行的平台的。在Solaris和Linux平台上将会抛出InterruptedIOException的异常,但是Windows上面不会有这种异常。所以,我们处理这种问题不能依靠于平台的实现。如:

package com.cnblogs.gpcuster

import java.net.*;
import java.io.*;

public abstract class InterruptibleReader extends Thread {

    private Object lock = new Object( );
    private InputStream is;
    private boolean done;
    private int buflen;
    protected void processData(byte[] b, int n) { }

    class ReaderClass extends Thread {

        public void run( ) {
            byte[] b = new byte[buflen];

            while (!done) {
                try {
                    int n = is.read(b, 0, buflen);
                    processData(b, n);
                } catch (IOException ioe) {
                    done = true;
                }
            }

            synchronized(lock) {
                lock.notify( );
            }
        }
    }

    public InterruptibleReader(InputStream is) {
        this(is, 512);
    }

    public InterruptibleReader(InputStream is, int len) {
        this.is = is;
        buflen = len;
    }

    public void run( ) {
        ReaderClass rc = new ReaderClass( );

        synchronized(lock) {
            rc.start( );
            while (!done) {
                try {
                    lock.wait( );
                } catch (InterruptedException ie) {
                    done = true;
                    rc.interrupt( );
                    try {
                        is.close( );
                    } catch (IOException ioe) {}
                }
            }
        }
    }
} 

另外,我们也可以使用InterruptibleChannel接口。 实现了InterruptibleChannel接口的类可以在阻塞的时候抛出ClosedByInterruptException。如:

package com.cnblogs.gpcuster

import java.io.BufferedReader;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.channels.Channels;

public class InterruptInput {   
    static BufferedReader in = new BufferedReader(
            new InputStreamReader(
            Channels.newInputStream(
            (new FileInputStream(FileDescriptor.in)).getChannel())));
    
    public static void main(String args[]) {
        try {
            System.out.println("Enter lines of input (user ctrl+Z Enter to terminate):");
            System.out.println("(Input thread will be interrupted in 10 sec.)");
            // interrupt input in 10 sec
            (new TimeOut()).start();
            String line = null;
            while ((line = in.readLine()) != null) {
                System.out.println("Read line:'"+line+"'");
            }
        } catch (Exception ex) {
            System.out.println(ex.toString()); // printStackTrace();
        }
    }
    
    public static class TimeOut extends Thread {
        int sleepTime = 10000;
        Thread threadToInterrupt = null;    
        public TimeOut() {
            // interrupt thread that creates this TimeOut.
            threadToInterrupt = Thread.currentThread();
            setDaemon(true);
        }
        
        public void run() {
            try {
                sleep(10000); // wait 10 sec
            } catch(InterruptedException ex) {/*ignore*/}
            threadToInterrupt.interrupt();
        }
    }
}

 

这里还需要注意一点,当线程处于写文件的状态时,调用interrupt()不会中断线程。

 

三、不提倡的stop()方法 
臭名昭著的stop()停止线程的方法已不提倡使用了,原因是什么呢?
 当在一个线程对象上调用stop()方法时,这个线程对象所运行的线程就会立即停止,并抛出特殊的ThreadDeath()异常。这里的“立即”因为太“立即”了,

    在java多线程编程中,线程的终止可以说是一个必然会遇到的操作。但是这样一个常见的操作其实并不是一个能够轻而易举实现的操作,而且在某些场景下情况会变得更复杂更棘手。

        Java标准API中的Thread类提供了stop方法可以终止线程,但是很遗憾,这种方法不建议使用,原因是这种方式终止线程中断临界区代码执行,并会释放线程之前获取的监控器锁,这样势必引起某些对象状态的不一致(因为临界区代码一般是原子的,不会被干扰的),具体原因可以参考资料[1]。这样一来,就必须根据线程的特点使用不同的替代方案以终止线程。根据停止线程时线程执行状态的不同有如下停止线程的方法。

Thread之九:stop

四、 处于运行状态的线程停止

        处于运行状态的线程就是常见的处于一个循环中不断执行业务流程的线程,这样的线程需要通过设置停止变量的方式,在每次循环开始处判断变量是否改变为停止,以达到停止线程的目的,比如如下代码框架:

private volatile Thread blinker;  
public void stop() {  
        blinker = null;  
}  
public void run() {  
        Thread thisThread = Thread.currentThread();  
        while (blinker == thisThread) {  
            try {  
                //业务流程  
            } catch (Exception e){}  
        }  
}

 

        如果主线程调用该线程对象的stop方法,blinker对象被设置为null,则线程的下次循环中blinker!=thisThread,因而可以退出循环,并退出run方法而使线程结束。将引用变量blinker的类型前加上volatile关键字的目的是防止编译器对该变量存取时的优化,这种优化主要是缓存对变量的修改,这将使其他线程不会立刻看到修改后的blinker值,从而影响退出。此外,Java标准保证被volatile修饰的变量的读写都是原子的。

        上述的Thread类型的blinker完全可以由更为简单的boolean类型变量代替。

五、 即将或正在处于非运行态的线程停止

        线程的非运行状态常见的有如下两种情况:

可中断等待:线程调用了sleep或wait方法,这些方法可抛出InterruptedException;

Io阻塞:线程调用了IO的read操作或者socket的accept操作,处于阻塞状态。

上面已经讲过。

3 处于大数据IO读写中的线程停止

         处于大数据IO读写中的线程实际上处于运行状态,而不是等待或阻塞状态,因此上面的interrupt机制不适用。线程处于IO读写中可以看成是线程运行中的一种特例。停止这样的线程的办法是强行close掉io输入输出流对象,使其抛出异常,进而使线程停止。

        最好的建议是将大数据的IO读写操作放在循环中进行,这样可以在每次循环中都有线程停止的时机,这也就将问题转化为如何停止正在运行中的线程的问题了。

4 在线程运行前停止线程

         有时,线程中的run方法需要足够健壮以支持在线程实际运行前终止线程的情况。即在Thread创建后,到Thread的start方法调用前这段时间,调用自定义的stop方法也要奏效。从上述的停止处于等待状态线程的代码示例中,stop方法并不能终止运行前的线程,因为在Thread的start方法被调用前,调用interrupt方法并不会将Thread对象的中断状态置位,这样当run方法执行时,currentThread的isInterrupted方法返回false,线程将继续执行下去。

         为了解决这个问题,不得不自己再额外创建一个volatile标志量,并将其加入run方法的最开头:

public void run() {  
        if (myThread == null) {  
           return; // stopped before started.  
        }  
        while(!Thread.currentThread().isInterrupted()){  
    //业务逻辑  
        }  
}  

 

         还有一种解决方法,也可以在run中直接使用该自定义标志量,而不使用isInterrupted方法判断线程是否应该停止。这种方法的run代码框架实际上和停止运行时线程的一样。

posted on 2014-05-14 21:37  duanxz  阅读(15572)  评论(0编辑  收藏  举报