Java 正确终止线程的方法

destroy() 为何被废弃

destroy() 只是抛出了一个 NoSuchMethodError,所以该方法无法终止线程:

@Deprecated  
public void destroy() {  
    throw new NoSuchMethodError();  
}  

stop() 为何被废弃

调用 stop() 方法是不安全的,这是因为当调用 stop() 方法时,会发生下面两件事:

  1. 即刻抛出 ThreadDeath 异常,在线程的 run() 方法内,任何一点都有可能抛出 ThreadDeath Error,包括在 catch 或 finally 语句中
  2. 会释放该线程所持有的所有的锁,而这种释放是不可控制的,非预期的
  3. 一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止
public class SynchronizedObject {
    private String name = "a";         // 省略 getter、setter
    private String password = "aa";    // 省略 getter、setter

    public synchronized void printString(String name, String password) {
        try {
            this.name = name;
            Thread.sleep(100000);
            this.password = password;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

public class MyThread extends Thread {
    private SynchronizedObject synchronizedObject;
    public MyThread(SynchronizedObject synchronizedObject) {
        this.synchronizedObject = synchronizedObject;
    }

    public void run() {
        synchronizedObject.printString("b", "bb");
    }
}

public class Run {
    public static void main(String args[]) throws InterruptedException {
        SynchronizedObject synchronizedObject = new SynchronizedObject();
        Thread thread = new MyThread(synchronizedObject);
        thread.start();
        Thread.sleep(500);
        thread.stop();
        System.out.println(synchronizedObject.getName() + "  " + synchronizedObject.getPassword());
    }
}

输出结果:

b aa

从上面的程序验证结果来看,thread.stop() 会释放该线程所持有的所有的锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用 thread.stop() 后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。

Java 中多线程锁释放的条件:

  1. 执行完同步代码块,就会释放锁(synchronized)
  2. 在执行同步代码块的过程中,遇到异常而导致线程终止,锁也会被释放(exception)
  3. 在执行同步代码块的过程中,执行了锁所属对象的 wait() 方法,这个线程会释放锁,进入对象的等待池(wait)

从上面的三点就可以看到 stop() 释放锁是在第二点的,通过抛出异常来释放锁,通过证明,这种方式是不安全的,不可靠的。

如何正确地停止一个线程

以下 2 种方法可以终止正在运行的线程:

  1. 使用退出标志,使线程正常退出,也就是当 run 方法完成后线程终止
  2. 使用 interrupt 方法中断线程

退出标志法

需要 while() 循环在某以特定条件下退出,最直接的办法就是设一个 boolean 标志,并通过设置这个标志来控制循环是否退出:

public class MyThread implements Runnable {
    private volatile boolean isCancelled;
    
    public void run() {
        while (!isCancelled) {
            //do something
        }
    }
    
    public void cancel() { isCancelled=true; }
}

注意,isCancelled 需要为 volatile,保证线程读取时 isCancelled 是最新数据

interrupt

如果线程是阻塞的,则不能使用退出标志法来终止线程。这时就只能使用 Java 提供的中断机制:

  • void interrupt()
    如果线程处于被阻塞状态(例如处于 sleep, wait, join 等状态),那么线程将立即退出被阻塞状态,并抛出一个 InterruptedException 异常
    如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true。被设置中断标志的线程将继续正常运行,不受影响

  • static boolean interrupted()
    测试当前线程(正在执行这一命令的线程)是否被中断。这一调用会将当前线程的中断状态重置为 false

  • boolean isInterrupted()
    测试线程是否被终止。不像静态的中断方法,这一调用不改变线程的中断状态

interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行。也就是说,一个线程如果有被中断的需求,那么就可以这样做:

① 在正常运行任务时,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程或者抛出 InterruptedException,使得线程停止的事件得以向上传播

Thread thread = new Thread(() -> {
    while (!Thread.interrupted()) {
        // do more work
    }
    // return or throw InterruptedException
});
thread.start();

// 一段时间以后
thread.interrupt();

thread.interrupted() 清除标志位是为了下次继续检测标志位。如果一个线程被设置中断标志后,选择结束线程那么自然不存在下次的问题,而如果一个线程被设置中断标识后,进行了一些处理后选择继续进行任务,而且这个任务也是需要被中断的,那么当然需要清除标志位了

② 在调用阻塞方法时正确处理 InterruptedException 异常(例如,catch 异常后就结束线程)

public class Interrupt {

    public static void main(String[] args) throws Exception {
        Interrupt t = new Interrupt();
        t.start();
    }

    public void start() {
        MyThread myThread = new MyThread();
        myThread.start();

        try {
            Thread.sleep(3000);
            myThread.cancel();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private class MyThread extends Thread {

        @Override
        public void run() {
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    System.out.println("test");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    System.out.println("interrupt");
                    // 抛出 InterruptedException 后中断标志被清除,标准做法是再次调用 interrupt 恢复中断
                    Thread.currentThread().interrupt();
                }
            }
            System.out.println("stop");
        }

        public void cancel(){
            interrupt();
        }
    }
}

Thread.sleep 这个阻塞方法,接收到中断请求,会抛出 InterruptedException,让上层代码处理。这时,可以什么都不做,但这等于吞掉了中断。因为抛出 InterruptedException 后,中断标记会被重新设置为 false!看 sleep() 的注释,也强调了这点:

@throws InterruptedException
     if any thread has interrupted the current thread. 
     The interrupted status of the current thread is 
     cleared when this exception is thrown.
public static native void sleep(long millis) throws InterruptedException;

记得这个规则:什么时候都不应该吞掉中断!每个线程都应该有合适的方法响应中断!

在接收到中断请求时,标准做法是执行 Thread.currentThread().interrupt() 恢复中断,让线程退出

从另一方面谈起,你不能吞掉中断,也不能中断你不熟悉的线程。如果线程没有响应中断的方法,你无论调用多少次 interrupt() 方法,也像泥牛入海

你可能感兴趣的:(Java 正确终止线程的方法)