如何中止Java中的线程

如何正确的中止一个运行中的线程

Java作为第一款官方声明支持多线程的编程语言,其早期提供的一些Api并不是特别的完善,所以可以看到Thread类中的一些早期方法都已经被标记上过时了,例如stop、resume,suspend,destory方法都被标记上过时的标签。那为了弥补这些缺失的功能,后续的Java提供了一些额外的方法,例如interrupt这样的方法。
stop方法会立即中止线程的运行并抛出错误,同时释放所有的锁,这可能产生数据安全问题(https://docs.oracle.com/javase/8/docs/technotes/guides/concurrency/threadPrimitiveDeprecation.html)。interrupt方法的实现方式更类似于给线程发送了一个信号(简单的理解为给线程设置了一个属性来标记是否应当停止当前线程,默认为false),线程接收到信号之后具体如何处理,是中止线程还是正常运行取决于运行的线程的具体逻辑。

中止线程的方法大致分两种

  • 通过Thread.interrupt()
  • 通过线程间的共享标记变量

通过interrupt()来中止一个线程

由于被中止的线程可能处于不同的状态,被终止的线程需要自行检测是否有人发信号给他,通知应该停止工作。

检测是否有人发信号的方法有两种

  • Thread.currentThread().isInterrupted() 返回当前线程上的标识位。推荐使用,该方法调用后不会修改标识;
  • Thread.interrupted() 返回当前线程上的标识位,同时调用后会立即把标识位修改位false。

终止一个runnable线程的最佳实践:

while(!Thread.currentThread().isInterrupted() && more work to do){
    do more work
}

处于runnable状态的线程被interrupt

看一个例子,对于一个循环打印running的线程,我们在main线程中调用了interrupt()方法,但是线程并没有按照我们预想的那样停下来,这样的代码并不是按照最佳实践的方式来运行的,我们没有把线程是否有接受到interrupt的信号放到while的判断去。

public class InterruptRunning {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while (true) {
                System.out.println("running");
            }
        });

        thread.start();
        try {
            Thread.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread.interrupt();
        System.out.println("Test End");
    }
}
//输出结果
//一直持续不断的输出running ,并没有像期待的那样2ms之后输出Test End

以上的代码的正确写法应该是,可以看到结果是我们期待的

public class InterruptRunning {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("running");
            }
        });

        thread.start();
        try {
            Thread.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread.interrupt();
        System.out.println("Test End");
    }
}
//输出结果
running
running
running
running
running
running
running
running
Test End

处于blocked/waiting/timed waiting状态的线程被interrupt

如果一个线程处于了阻塞状态(如线程调用了thread.sleep、thread.join、thread.wait、1.5中的condition.await、以及可中断的通道上的 I/O 操作方法后可进入阻塞状态),则在线程在检查中断标示时如果发现中断标示为true,则会在这些阻塞方法(sleep、join、wait、1.5中的condition.await及可中断的通道上的 I/O 操作方法)调用处抛出InterruptedException异常,并且在抛出异常后立即将线程的中断标示位清除,即重新设置为false。抛出异常是为了线程从阻塞状态醒过来,并在结束线程前让程序员有足够的时间来处理中断请求。

即先后做了三件事

  • 使线程状态由阻塞状态变为runnable
  • 抛出InterruptedException异常
  • 如果InterruptedException异常被捕获,会立即将中断标识由true设置为false

举个例子,我们在循环的时候通过中断标识位来决定是否循环,并且将try catch代码块置于while循环内部。此时中断产生的异常会先被try catch捕获,然后中断标识位会被设置位false;while循环不会中止,线程依旧执行。
在main线程中调用standardInterrupt.interrupt();尝试中止中止StandardInterrupt线程,线程的输出内容说明了其捕获到了异常,但是检查标识发现值位false说明已经被重置了,于是可以看到while循环条件满足,线程不会停止,依旧运行下去。

public class StandardInterrupt extends Thread{
    public static void main(String[] args) throws InterruptedException {
        StandardInterrupt standardInterrupt = new StandardInterrupt();
        System.out.println("main thread --> start running");
        standardInterrupt.start();
        Thread.sleep(3000);
        System.out.println("main thread --> send signal");
        standardInterrupt.interrupt();
        Thread.sleep(3000);
        System.out.println("main thread --> stop application");
    }

    public void run(){
        while(!Thread.currentThread().isInterrupted()){
            System.out.println("active thread --> i am working");
            try {
                Thread.sleep(1000);
            }catch (InterruptedException e){
                System.out.println("active thread --> detective interrupt");
                System.out.println("active thread --> check the signal: " +Thread.currentThread().isInterrupted());
            }
        }
        System.out.println("active thread --> finish my working");
    }
}

//输出
main thread --> start running
active thread --> i am working
active thread --> i am working
active thread --> i am working
main thread --> send signal
active thread --> detective interrupt
active thread --> check the signal: false
main thread --> stop application
active thread --> i am working
active thread --> i am working
active thread --> i am working
......

我们再稍作修改如上的代码,在捕获异常之后,主动的再次触发一次interrupt(),这可以将被自动设置回false的中断标识位再次变为true。
这次可以看到active thread --> finish my working输出,因为while循环去检查标识的时候,Thread.currentThread().isInterrupted() = true;

public class StandardInterrupt extends Thread{
    public static void main(String[] args) throws InterruptedException {
        StandardInterrupt standardInterrupt = new StandardInterrupt();
        System.out.println("main thread --> start running");
        standardInterrupt.start();
        Thread.sleep(3000);
        System.out.println("main thread --> send signal");
        standardInterrupt.interrupt();
        Thread.sleep(3000);
        System.out.println("main thread --> stop application");
    }

    public void run(){
        while(!Thread.currentThread().isInterrupted()){
            System.out.println("active thread --> i am working");
            try {
                Thread.sleep(1000);
            }catch (InterruptedException e){
                System.out.println("active thread --> detective interrupt");
                System.out.println("active thread --> check the signal: " +Thread.currentThread().isInterrupted());
                Thread.currentThread().interrupt();
            }
        }
        System.out.println("active thread --> finish my working");
    }
}

//输出
main thread --> start running
active thread --> i am working
active thread --> i am working
active thread --> i am working
main thread --> send signal
active thread --> detective interrupt
active thread --> check the signal: false
active thread --> finish my working
main thread --> stop application
......

另一种修改方式,是将try catch放在while循环外面,这样跳出while的原因是因为检测到异常。然后try catch捕获异常,修改标识位。

public class StandardInterrupt extends Thread {
    public static void main(String[] args) {
        StandardInterrupt standardInterrupt = new StandardInterrupt();
        System.out.println("main thread --> start running");
        standardInterrupt.start();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("main thread --> send signal");
        standardInterrupt.interrupt();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("main thread --> stop application");
    }

    public void run() {
        try {
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("active thread --> i am working");
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            System.out.println("active thread --> detective interrupt");
            System.out.println("active thread --> check the signal: " + Thread.currentThread().isInterrupted());
        }
        System.out.println("active thread --> finish my working");
    }
}

//输出
main thread --> start running
active thread --> i am working
active thread --> i am working
active thread --> i am working
main thread --> send signal
active thread --> detective interrupt
active thread --> check the signal: false
active thread --> finish my working
main thread --> stop application
......

总结一下对于要处理这种blocked状态的线程应该正确书写代码的格式

public void run() {
    try {
        ...
        /*
         * 不管循环里是否调用过线程阻塞的方法如sleep、join、wait,这里还是需要加上
         * !Thread.currentThread().isInterrupted()条件,虽然抛出异常后退出了循环,显
         * 得用阻塞的情况下是多余的,但如果调用了阻塞方法但没有阻塞时,这样会更安全、更及时。
         */
        while (!Thread.currentThread().isInterrupted()&& more work to do) {
            do more work 
        }
    } catch (InterruptedException e) {
        //线程在wait或sleep期间被中断了
    } finally {
        //线程结束前做一些清理工作
    }
}

或者

public void run() {
    while (!Thread.currentThread().isInterrupted()&& more work to do) {
        try {
            ...
            sleep(delay);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();//重新设置中断标示
        }
    }
}

处于blocked状态的线程

  • 由synchronized,或者是由reentrantLock.lock()导致的锁是无法响应interrupt()方法的,他们只有在获取锁之后正常运行的时候才能响应。
  • reentrantLock.tryLock(long timeout, TimeUnit unit),reentrantLock.lockInterruptibly()方法所导致的blocked状态却是能正确响应interrupt()方法的,锁住的线程将抛出一个InterruptedException异常,这是一个非常有用的特性,因为它允许程序打破死锁。
posted @ 2020-09-24 18:58  pikzas  阅读(269)  评论(0编辑  收藏  举报