Java如何停止一个线程

Posted 风在哪

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java如何停止一个线程相关的知识,希望对你有一定的参考价值。

Java如何停止一个线程

在Java中,停止一个正在运行的线程,我们可以通过如下方法实现:

  • 设置一个volatile类型的变量,通过判断变量的值来确定run方法是否运行完成正常退出
  • 通过interrupt()方法来通知,给线程打上一个终止的标志,但是具体的停止逻辑需要在线程内部实现

此外还有一种方法是调用stop()方法来停止线程,但是这个方法不安全:它会释放该线程持有的所有锁,一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁突然释放,那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。suspend方法:容易发生死锁,虽然调用suspend方法目标线程会停下来,但是仍然持有之前的锁,其他线程都不能访问锁定的资源,就会造成死锁,除非被挂起的线程恢复运行。

所以这里只提及了上述的两种方法。

volatile变量

我们可以自己实现的线程中设置了volatile的变量,然后在线程内部判断volatile是否满足运行的条件,满足就继续运行,不满足就退出线程。我们可以在调用线程的地方设置这个volatile变量,通过对volatile的设置达到停止线程的目的。

这里为什么使用volatile变量呢,因为volatile变量会保证多线程之间的可见性,变量的值一经修改所有的线程都能看到这个改变,根据这一性质才能停止线程的运行。

一个简单的例子:

public class ThreadInterrupt extends Thread{
    private volatile boolean exit = false;

    @Override
    public void run() {
        while (!exit) {
            System.out.println("Running!");
        }
    }


    public static void main(String[] args) throws Exception {
        ThreadInterrupt thread = new ThreadInterrupt();
        thread.start();
        sleep(10);
        thread.exit = true;
        thread.join();
        System.out.println("线程已退出!");
    }
}

但是,使用volatile变量停止线程还是不够全面,某些情况下索然可用,但是某些情况下可能会有严重的问题。

Java并发编程实战指出了其缺陷:如果我们遇到了线程长时间阻塞(例如生产者消费者中就存在这样的情况),就没办法及时唤醒它,或者永远都无法唤醒该线程。此时我们就可以使用interrupt了,interrupt在设计之初就把wait等长阻塞作为一种特殊情况考虑在内,所以可以在线程内部有sleep或者wait方法时使用interrupt方法停止线程。

interrupt()方法

我们首先看看线程中断的几个方法:

  1. java.lang.Thread#interrupt():中断目标线程,给目标线程发出一个中断信号,线程被打上中断标记
  2. java.lang.Thread#isInterrupted():判断目标线程是否被中断,不会清除中断标记
  3. java.lang.Thread#interrupted():判断目标线程是否被中断,会清除中断标记

image-20210511125815169

从上图源码可以发现,interrupted()方法和isInterrupted()方法都调用了本地isInterrupted()方法,该方法根据传入的参数决定是否清除中断状态,如果ClearInterrupted为true的话就要清除线程的中断标志,如果为false则不会清除。

使用interrupt暂停线程又有两种场景,第一种场景是线程的run方法中没有sleep和wait等会响应中断的方法,第二种场景就是有sleep和wait方法。我们首先来看看run方法中不包含sleep和wait方法的场景。

run方法不包含sleep和wait等响应中断的方法

我们首先看一个错误的示范。

首先我们看看打印Integ.MAX_VALUE/10以内的1000000倍数:

public static void main(String[] args) throws Exception {
    int i = 0;
    while (i < Integer.MAX_VALUE / 10) {
        if (i % 1000000 == 0) {
            System.out.println(i + "是1000000的倍数");
        }
        i++;
    }
}

它的结果如下所示:

image-20210511104821120

再来看看错误的使用interrupt方法的例子:

public class Interrupted implements Runnable {

    @Override
    public void run() {
        int i = 0;
        while (i < Integer.MAX_VALUE / 10) {
            if (i % 1000000 == 0) {
                System.out.println(i + "是1000000的倍数");
            }
            i++;
        }
        System.out.println("任务成功执行完毕");
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new Interrupted());
        // 开启线程
        thread.start();
        // 中断子线程
        thread.interrupt();
    }
}

image-20210511104607776

可以看到它的运行效果,其实和正常运行是一样的,也就是说线程没有被终止。

那么正确的方法应该是什么呢,我们只需要改动run方法即可,在run方法中判断线程终止的状态,如果是终止的话就退出run方法。前面说过interrupt方法只是打了一个标志,标志线程被中断了,那么我们可以根据这个标志判断是否需要停止线程,代码如下:

public class Interrupted implements Runnable {

    @Override
    public void run() {
        int i = 0;
        while (!Thread.currentThread().isInterrupted() && i < Integer.MAX_VALUE / 10) {
            if (i % 1000000 == 0) {
                System.out.println(i + "是1000000的倍数");
            }
            i++;
        }
        System.out.println("任务成功执行完毕");
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new Interrupted());
        // 开启线程
        thread.start();
        // 给子线程增加运行时间,不加这句代码线程会直接被终止
        Thread.sleep(500);
        // 中断子线程
        thread.interrupt();
    }
}

运行结果如下:

image-20210511105524098

可以看到线程成功被终止了!

run方法包含sleep和wait等响应中断的方法

当我们调用响应中断的方法时,如果此时调用了interrupt方法,响应中断的方法就会抛出异常,那么我们捕获并处理这个异常就能使线程正常的退出了,例如:

public class Interrupted implements Runnable {

    @Override
    public void run() {
        int i = 0;
        while (!Thread.currentThread().isInterrupted() && i < 1000) {
            if (i % 200 == 0) {
                System.out.println(i + "是200的倍数");
            }
            i++;
        }
        try {
            // sleep方法响应中断,会抛出InterruptedException异常
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("任务成功执行完毕");
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Interrupted());
        // 开启线程
        thread.start();
        // 给子线程增加运行时间,等待while执行完毕
        Thread.sleep(50);
        // 中断子线程
        thread.interrupt();
    }
}

其运行结果如下:

image-20210511110411649

可以看到我们的while循环完成之后,在sleep进行了阻塞,此时进行中断的话,sleep响应中断,抛出InterruptedException异常,并打印出异常堆栈。

但是如果我们将sleep()放在while循环内部,这个方法还会成功吗?例如:

public class Interrupted implements Runnable {

    @Override
    public void run() {
        int i = 0;
        while (!Thread.currentThread().isInterrupted() && i < 1000) {
            if (i % 10 == 0) {
                System.out.println(i + "是10的倍数");
            }
            i++;
            try {
                // sleep方法响应中断,会抛出InterruptedException异常
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("任务成功执行完毕");
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Interrupted());
        // 开启线程
        thread.start();
        // 给子线程增加运行时间
        Thread.sleep(50);
        // 中断子线程
        thread.interrupt();
    }
}

其运行结果如下:

image-20210511110753597

我们的预期是,当线程进入while循环时,会休眠500毫秒,当50毫秒时主线程发起了中断信号,sleep响应中断,打印异常堆栈,下次再进入while循环时,因为线程被设置为中断状态,所以会直接退出。

但是实际效果却不是这样,再次进入while循环时,还会继续执行相应的逻辑。这是为什么呢?

我们可以看看sleep的注释:

image-20210511111506783

通过其注释我们可以发现,当sleep相应中断并抛出异常后,会清除线程的中断状态。所以这也就是为什么我们在while循环中发生上述的情况。

所以对于中断,我们需要传递中断,并对中断进行处理,或者处理响应中断方法抛出的异常。

如果无法传递中断就恢复中断。就像上面sleep在while循环中会清除中断一样,我们可以在catch中再恢复中断。例如:

	@Override
    public void run() {
        int i = 0;
        while (!Thread.currentThread().interruptisInterrupted() && i < 1000) {
            if (i % 10 == 0) {
                System.out.println(i + "是10的倍数");
            }
            i++;
            try {
                // sleep方法响应中断,会抛出InterruptedException异常
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
                // 尝试恢复中断
                Thread.currentThread().interrupt();
            }
        }
        System.out.println("任务成功执行完毕");
    }

总结

我们在实际使用中,如果要停止一个线程的话,尽量使用interrupt()方法,尽量减少volatile变量的使用。

参考

一只特立独行的猪丶

以上是关于Java如何停止一个线程的主要内容,如果未能解决你的问题,请参考以下文章

Java 如何正确停止一个线程

Android Studio - 如何从片段中停止 handler.postDelayed?

c#线程停止

如何正确停止线程

如何停止在 Java 中的阻塞读取操作中等待的线程?

如何通过按下 Java Swing 按钮来停止线程?