如何正确停止线程之 interrupt 的使用

原理介绍


  • 使用 interrupt 来通知,而不是强制停止

Java 中停止线程的原则是什么


  • 在 Java 中,最好的停止线程的方式是使用中断 interrupt,但是这仅仅是会通知到被终止的线程“你该停止运行了”,被终止的线程自身拥有决定权(决定是否、以及何时停止),这依赖于请求停止方和被停止方都遵守一种约定好的编码规范。
  • 任务和线程的启动很容易。在大多数时候,我们都会让它们运行直到结束,或者让它们自行停止。然而有时候我们希望提前结束任务或线程或许是因为用户取消了操作或者服务需要被快速关闭,或者是运行超时或出错了。
  • 要使任务和线程能安全、快速、可靠地停止下来,并不是一件容易的事。Java 没有提供任何机制来安全地终止线程;但它提供了中断(Interruption),这是一种协作机制,能够使一个线程终止另—个线程的当前工作
  • 这种协作式的方法是必要的,我们很少希望某个任务、线程或服务立即停止,因为这种立即停止会使共享的数据结构处于不一致的状态。相反,在编写任务和服务时可以使用种协作的方式当需要停止时它们首先会清除当前正在执行的工作然后再结束。这提供了更好的灵活性,因为任务本身的代码比发出取消请求的代码更清楚如何执行清除工作。
  • 生命周期结束(End- of-Lifecycle)的问题会使任务、服务以及程序的设计和实现等过程变得复杂,而这个在程序设计中非常重要的要素却经常被忽略。一个在行为良好的软件与勉强运的软件之间的最主要区别就是,行为良好的软件能很完善地处理失败、关闭和取消等过程。
  • 接下来将给岀各种实现取消和中断的机制,以及如何编写任务和服务,使它们能对取消请求做出响应。

正确的停止方法:interrupt


通常线程会在什么情况下停止普通情况?

示例一:run 方法内没有 sleep 或 wait 方法时,停止线程

public class RightWayStopThreadWithoutSleep implements Runnable {

    @Override
    public void run() {
        int num = 0;
        while (!Thread.currentThread().isInterrupted() && num <= Integer.MAX_VALUE / 2) {
            if (num % 10000 == 0) {
                System.out.println(num + "是10000的倍数");
            }
            num++;
        }
        System.out.println("任务运行结束了");
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new RightWayStopThreadWithoutSleep());
        thread.start();
        Thread.sleep(2000);
        thread.interrupt();
    }
}

打印结果:

...
279980000是10000的倍数
279990000是10000的倍数
280000000是10000的倍数
280010000是10000的倍数
任务运行结束了

虽然主线程调用了 thread.interrupt() 方法,但他只是把子线程标记为中断,代码中还需要不停地去判断是否被中断,即:Thread.currentThread().isInterrupted()

线程可能被阻塞

示例二:带有 sleep 的中断线程的写法

public class RightWayStopThreadWithSleep {

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
            int num = 0;
            try {
                while (num <= 300 && !Thread.currentThread().isInterrupted()) {
                    if (num % 100 == 0) {
                        System.out.println(num + "是100的倍数");
                    }
                    num++;
                }
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };
        Thread thread = new Thread(runnable);
        thread.start();
        Thread.sleep(500);
        thread.interrupt();
    }
}

打印结果:

0是100的倍数
100是100的倍数
200是100的倍数
300是100的倍数
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.hd.thread.stop.RightWayStopThreadWithSleep.lambda$main$0(RightWayStopThreadWithSleep.java:21)
	at java.lang.Thread.run(Thread.java:745)

Thread.sleep() 阻塞过程中能够检测到中断标记,而抛出中断异常。

如果线程在每次迭代后都阻塞

示例三:如果在执行过程中,每次循环都会调用 sleep 或 wait 等方法,那么不需要每次迭代都检查是否已中断,sleep 会响应中断

public class RightWayStopThreadWithSleepEveryLoop {

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
            int num = 0;
            try {
                while (num <= 10000) {
                    if (num % 100 == 0) {
                        System.out.println(num + "是100的倍数");
                    }
                    num++;
                    Thread.sleep(10);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };
        Thread thread = new Thread(runnable);
        thread.start();
        Thread.sleep(5000);
        thread.interrupt();
    }
}

打印结果:

0是100的倍数
100是100的倍数
200是100的倍数
300是100的倍数
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.hd.thread.stop.RightWayStopThreadWithSleepEveryLoop.lambda$main$0(RightWayStopThreadWithSleepEveryLoop.java:20)
	at java.lang.Thread.run(Thread.java:745)

这个案例中我们不再需要检查是否中断,sleep() 阻塞时候会自动检测中断,即不再需要:Thread.currentThread().isInterrupted()

while 内 try/catch 的问题

示例四:如果 while 里面放 try/catch,会导致中断失效

public class CantInterrupt {

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
            int num = 0;
            while (num <= 10000 && !Thread.currentThread().isInterrupted()) {
                if (num % 100 == 0) {
                    System.out.println(num + "是100的倍数");
                }
                num++;
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Thread thread = new Thread(runnable);
        thread.start();
        Thread.sleep(5000);
        thread.interrupt();
    }
}

打印结果:

0是100的倍数
100是100的倍数
200是100的倍数
300是100的倍数
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.hd.thread.stop.CantInterrupt.lambda$main$0(CantInterrupt.java:20)
	at java.lang.Thread.run(Thread.java:745)
400是100的倍数
500是100的倍数
600是100的倍数
...

Thread.sleep() 检测到的中断信号抛出的异常被捕获了。

实际开发中的两种最佳实践

  • 优先选择:传递中断

示例五:最佳实践:catch 了 InterruptedExcetion 之后的优先选择:在方法签名中抛出异常 那么在 run() 就会强制 try/catch

public class RightWayStopThreadInProd implements Runnable {

    @Override
    public void run() {
        while (true && !Thread.currentThread().isInterrupted()) {
            System.out.println("go");
            try {
                throwInMethod();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                //保存日志、停止程序
                System.out.println("保存日志");
                e.printStackTrace();
            }
        }
    }

    private void throwInMethod() throws InterruptedException {
        Thread.sleep(2000);
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new RightWayStopThreadInProd());
        thread.start();
        Thread.sleep(1000);
        thread.interrupt();
    }
}

打印结果:

go
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.hd.thread.stop.RightWayStopThreadInProd.throwInMethod(RightWayStopThreadInProd.java:27)
	at com.hd.thread.stop.RightWayStopThreadInProd.run(RightWayStopThreadInProd.java:16)
	at java.lang.Thread.run(Thread.java:745)
保存日志

处理中断的最好方法是什么?

优先选择在方法上抛出异常。用 throws Interrupted Exception 标记你的方法,不采用try语句块捕获异常,以便于该异常可以传递到顶层,让 run 方法可以捕获这一异常,例如

    private void throwInMethod() throws InterruptedException {
        Thread.sleep(2000);
    }

由于 run 方法内无法抛出 checked Exception(只能用 try catch),顶层方法必须处理该异常,避免了漏掉或者被吞掉的情况,增强了代码的健壮性。

  • 不想或无法传递:恢复中断

示例六:最佳实践 2:在 catch 子语句中调用 Thread.currentThread().interrupt() 来恢复设置中断状态,以便于在后续的执行中,依然能够检查到刚才发生了中断回到刚才 RightWayStopThreadInProd 补上中断,让它跳出。

public class RightWayStopThreadInProd2 implements Runnable {

    @Override
    public void run() {
        while (true) {
            if (Thread.currentThread().isInterrupted()) {
                System.out.println("Interrupted,程序运行结束");
                break;
            }
            reInterrupt();
        }
    }

    private void reInterrupt() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new RightWayStopThreadInProd2());
        thread.start();
        Thread.sleep(1000);
        thread.interrupt();
    }
}

打印结果:

Interrupted,程序运行结束
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.hd.thread.stop.RightWayStopThreadInProd2.reInterrupt(RightWayStopThreadInProd2.java:25)
	at com.hd.thread.stop.RightWayStopThreadInProd2.run(RightWayStopThreadInProd2.java:19)
	at java.lang.Thread.run(Thread.java:745)

如果不能抛出中断,要怎么做?

如果不想或无法传递 InterruptedEXception(例如用 run 方法的时候,就不让该方法 throws InterruptedEXception),那么应该选择在 catch子句中调用 Thread.currentThread().interrupt() 来恢复设置中断状态,以便于在后续的执行依然能够检查到刚才发生了中断。看上面代码,线程在 sleep 期间被中断,并且由 catch 捕获到该中断并重新设置了中断状态,以便于可以在下—个循环的时候检测到中断状态,正常退出。

  • 不应屏蔽中断

响应中断的方法总结列表

  • Object.wait() / wait(long) / wait(long, int)
  • Thread.sleep(long) / sleep(long, int)
  • Thread.join() / join( long) / join(long, int)
  • java.util.concurrent.BlockingQueue.take() / put (E)
  • java.util.concurrent.locks.Lock.lockInterruptibly()
  • java.util.concurrent.CountDownLatch.await
  • java.util.concurrent.CyclicBarrier.await
  • java.util.concurrent.Exchanger.exchange(v)
  • java.nio.channels.InterruptibleChannel相关方法
  • java.nio.channels.Selector的相关方法

笔记来源:慕课网悟空老师视频《Java并发核心知识体系精讲》

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

✦昨夜星辰✦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值