如何优雅地终止一个线程

背景

在开发某个组件时,需要定期从数据库中拉取数据。由于整个逻辑非常简单,因此就启用了一个子线程(Thread)使用while循环+线程休眠来定期更新。
这时候我又想起一个老生常谈的问题——如何优雅地停止线程?

思路

大家都知道,Thread的stop方法早已废除,在高速上一脚猛刹,很可能人仰马翻,太危险。
时至今日,这个问题早已有常规解决方案,即检测线程的interrupt变量值对应中断状态(下简称interrupt状态)时停止循环,也就是类似如下的形式:

    while(!中断状态) {  // interrupt状态
        // do sth...
    }

这个方案的确非常常规,但每次到用的时候总会忧心忡忡——要知道跟线程interrupt状态相关的方法可是有多种,他们有什么区别?这样做能保证正常中断吗?Java进程运行结束的时候这个线程会终止吗(涉及到Tomcat的重启问题)?

中断相关的方法

Thread.interrupted()
实际上调用的是Thread.currentThread().isInterrupted(true)

Thread.currentThread().isInterrupted()
实际上调用的是Thread.currentThread()这个对象的isInterrupted()

thread.isInterrupted()
实际上调用的是thread.isInterrupted(false)

Thread.currentThread().interrupt()
同样的,调用的是Thread.currentThread()这个对象的interrupt()方法。

thread.interrupt()
代码如下:

    public void interrupt() {
        if (this != Thread.currentThread())
            checkAccess();

        synchronized (blockerLock) {
            Interruptible b = blocker;
            if (b != null) {
                interrupt0();           // Just to set the interrupt flag
                b.interrupt(this);
                return;
            }
        }
        interrupt0();
    }

分析

本质上来说,在Thread类中线程中断相关的方法有3个:

  1. Thread.interrupted()
  2. thread.isInterrupted()
  3. thread.interrupt()

尽管Thread.interrupted()thread.isInterrupted()最终都是调用Thread对象也就是thread的isInterrupted方法,只是参数不同,但thread.isInterrupted(boolean)方法是个私有方法,即调用该方法只能通过Thread.interrupted()thread.isInterrupted()
没辙,还不如干脆就当做不同的方法来看。
ps. 显然,Thread.currentThread()对象肯定是目前在执行这个循环的线程对象,也就是谁调的这个方法所属的线程,包含主线程。

thread.isInterrupted(boolean ClearInterrupted)
这是一个native方法,注释中写道:

检测某个线程是否被中断。ClearInterrupted参数决定了是否清理interrupt状态。

很好理解,调用isInterrupted(true)会返回当前的interrupt状态,并清理interrupt状态(即如果目前是中断状态,会修改为非中断状态)。
反之调用isInterrupted(false)不会清理interrupt状态。

thread.interrupt()
源码如前述,注释大意如下:

中断这个Thread对象。
线程可以中断自身,但中断其他线程需要通过checkAccess()方法检查。(怎么检查的没细看)
如果线程在阻塞状态,会清除interrupt状态并抛出异常,不同的阻塞类型会抛出不同的异常,比如wait()、sleep()等会抛出InterruptedException;否则,会设置interrupt状态为中断状态。

源码调用了native方法interrupt0(),我们就不更深入分析了。从注释可以看出来线程是否在阻塞状态,会影响到interrupt()方法的行为:

  • 如果线程在阻塞状态,这个方法会清除interrupt状态并抛异常
  • 如果线程未在阻塞状态,这个方法仅仅是设置了interrupt状态

造成的影响
让我们回到开头的解决方案。

    while(!中断状态) {  // interrupt状态
        // do sth...
    }

总的来说,我们面临两个问题:

  1. 如何合理获取interrupt状态?
  • Thread.interrupted()获取并清除状态
  • thread.isInterrupted()获取并不清除状态
  1. 如果while循环中有阻塞逻辑,会不会导致我们的解决方案有差异?
  • 阻塞时调用thread.interrupt()方法会清理interrupt状态并抛异常
  • 非阻塞时调用thread.interrupt()设置interrupt状态并不抛异常

毫无疑问,获取interrupt状态两种方法都可以。但某些错误用法会导致线程无法中断。
Bad Case1:错误使用Thread.interrupted(),导致线程无法中断

    while(!Thread.interrupted()) {  // interrupt状态被清理,死循环
        // do sth...
        if (Thread.interrupted()) { // 这里的判断清理了interrupt状态
            // 做一些中断的后续动作
        }
    }

这种Case在使用正则表达式匹配(matcher.find()方法)时也要注意。

而在调用thread.interrupt()方法并搭配获取interrupt状态的方法时,就需要考虑阻塞问题了。
Bad Case2:未考虑阻塞导致interrupt状态被吞,线程无法中断

    while(!Thread.interrupted()) {  // interrupt状态被吞,死循环
        // do sth...
        try {
            // 阻塞时调用interrupt()方法,只抛异常不设置interrupt状态
            Thread.sleep(10);
        } catch (InterruptedException e) {
            // do sth...
        }
    }

Bad Case3:仅使用interrupt()方法并不能中断线程

    while(true) {   // 死循环
        // do sth...
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            // do sth...
            // 仅使用interrupt()方法,就像调用System.gc()一样,只是进行通知设置状态,并不表示动作执行
            Thread.currentThread().interrupt();
        }
    }

实验验证

死循环3例

    // Bad Case 1
    public static void main(String[] args) throws Exception {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 0;
                while(!Thread.interrupted() && i++ < 50000) {   // interrupt状态被清理,死循环
                    System.out.println(i);
                    if (Thread.interrupted()) { // 这里的判断清理了interrupt状态
                        System.out.println("interrupted");
                    }
                }
            }
        });
        thread.start();
        Thread.sleep(5);
        thread.interrupt(); // 中断
    }
    // 输出结果:输出1~5w并在中间某处输出了interrupted,说明中断未生效,死循环

    // Bad Case 2
    public static void main(String[] args) throws Exception {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 0;
                while(!Thread.interrupted() && i++ < 50) {  // interrupt状态被吞,死循环
                    System.out.println(i);
                    try {
                        // 阻塞时调用interrupt()方法,只抛异常不设置interrupt状态
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        thread.start();
        Thread.sleep(5);
        thread.interrupt();
    }
    // 输出结果:输出1~50并在中间某处输出了异常信息,说明中断未生效,死循环

    // Bad Case 3
    public static void main(String[] args) throws Exception {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 0;
                while(true && i++ < 50) {   // 死循环
                    System.out.println(i);
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        System.out.println("interrupted exception");
                        // 仅使用interrupt()方法,就像调用System.gc()一样,只是进行通知设置状态,并不表示动作执行
                        Thread.currentThread().interrupt();
                    }
                }
            }
        });
        thread.start();
        Thread.sleep(5);
        thread.interrupt();
    }
    // 输出结果:从1~50中间开始轮番输出数字和"interrupted exception",说明中断未生效,死循环

验证结果符合预期。

正确的结束方式4例

    // Case 1 无阻塞的情况
    public static void main(String[] args) throws Exception {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 0;
                while(!Thread.interrupted()) {
                    System.out.println(i++);
                }
            }
        });
        thread.start();
        Thread.sleep(50);
        thread.interrupt();
    }
    // 输出结果:本机验证输出0~11013,成功中断
    
    // Case 2 有阻塞的情况
    public static void main(String[] args) throws Exception {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 0;
                while(!Thread.interrupted()) {
                    System.out.println(i++);
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        // 在这里调了一次interrupt(),保证线程未处于阻塞状态
                        Thread.currentThread().interrupt();
                    }
                }
            }
        });
        thread.start();
        Thread.sleep(50);
        thread.interrupt();
    }
    // 输出结果:本机验证输出0~50并打印出InterruptedException,成功中断
    
    // Case 3 使用volatile变量控制线程同步
public class Test extends Thread {
    // 利用volatile变量的机制
    private volatile boolean stop;
    @Override
    public void run() {
        int i = 0;
        while(!stop) {
            System.out.println(i++);
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                System.out.println("interrupted exception");
            }
        }
    }
    public static void main(String[] args) throws Exception {
        Test thread = new Test();
        thread.start();
        Thread.sleep(5);
        thread.stop = true;
        // 等5ms再调中断方法,确认在调interrupt方法时线程是否已中断
        Thread.sleep(5);
        thread.interrupt();
    }
}
    // 输出结果:本机验证输出0~4,未输出"interrupted exception",成功控制线程终止
    
    // Case 4 无脑但安全
    public static void main(String[] args) throws Exception {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    int i = 0;
                    while (true) {
                        System.out.println(i++);
                        Thread.sleep(1);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread.start();
        Thread.sleep(50);
        thread.interrupt();
    }
    // 输出结果:本机验证输出0~49并打印出InterruptedException,成功中断

结论

  1. 调用thread.interrupt()方法不一定能中断线程
  2. 阻塞状态被中断会抛异常,但不会设置interrupt状态
  3. interrupt状态设置为中断不代表线程没在运行,类似System.gc()只是通知一下,main方法也可以中断
  4. Thread.interrupted()方法会清理interrupt状态

无阻塞的情况下要保证interrupt状态仅在while判断时重置,不能受其他部分影响。

    while(!Thread.interrupted()) {  // interrupt状态,要保证循环中不会重置这个值
        // do sth...
    }

有阻塞的情况下要保证interrupt状态不被吞,可以在catch块中再次调用interrupt()方法设置interrupt状态。

    while(!Thread.interrupted()) {  // interrupt状态,要保证循环中不会重置这个值
        // do sth...
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            // do sth...
            // 在这里调了一次interrupt(),此时线程未处于阻塞状态,会设置interrupt状态
            Thread.currentThread().interrupt();
        }
    }

还可以通过volatile变量控制线程中的循环,不过这种方式略微麻烦了些,如果不了解原理还很容易错,不太推荐使用。
当然,还可以无脑try catch,视情况而定,不太推荐使用。

补充

如何确保JVM关停时终止该线程

上述Case中的线程,如果不刻意中断,将会导致程序循环,无法正常结束(比如Tomcat的shutdown过程无法停止),只能强行关停(kill -9)Java进程。通过以下方法可以确保程序终止时终止该程序。

使用守护线程
就是调用thread.setDaemon(true)将线程设置为守护线程,会在主线程终止后自动终止。
注意必须在线程启动前设置

使用ShutdownHook
通过添加中断逻辑到Hook中,可以在关闭程序(kill -15,ctrl+c)时运行这些中断逻辑。

        Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
            @Override
            public void run() {
                if (!thread.isInterrupted()) {
                    thread.interrupt();
                }
            }
        }));

不过直接调用thread.interrupt()都无法关闭的Bad Case,这种方式显然也无法关闭。

参考资料

利用 java.lang.Runtime.addShutdownHook() 钩子程序,保证java程序安全退出 - baibaluo - 博客园

本文搬自我的博客,欢迎参观!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容