• 4.如何终止线程


    目录

    1 如何终止正在执行的线程

    2 如何终止被阻塞的线程

    3.总结


    线程终止是一个稍微复杂的问题,我们分运行状态和阻塞状态两种情况讨论。

    1 如何终止正在执行的线程

    首先我们思考一下,线程在什么情况下会终止?一般来说有如下几种情况:

    第一种:当run方法完成后线程终止

    run方法中的内容执行完后线程一般就自动结束了。

    第二种:使用stop方法强行终止

    该方法会强制关闭正在执行的线程,这种方法是不推荐的,因为假如很多指令正在执行,很多重要操作可能尚未完成,如果强制停止会导致潜在问题,例如一些清理性的工作没完成,如文件,数据库等的关闭。

    也就说调用 stop() 方法会立即释放该线程所持有的所有的锁,导致数据得不到同步,出现数据不一致的问题。看个例子:

    1. public class ThreadStopExample extends Thread {
    2.   @Override
    3.   public void run() {
    4.       try {
    5.           for (int i = 0; i < 1000000; i++) {
    6.               System.out.println("running code " + i);
    7.           }
    8.           System.out.println("the code finished");
    9.       }catch (Throwable e){
    10.           e.printStackTrace();
    11.       }
    12.   }
    13.   public static void main(String[] args) throws InterruptedException {
    14.       Thread thread = new ThreadStopExample();
    15.       thread.start();
    16.       Thread.sleep(10);
    17.       thread.stop();
    18.   }
    19. }

    该代码在执行的时候会抛出如下异常:

    1. running code 327
    2. running code 328
    3. running code 329
    4. java.lang.ThreadDeath
    5. at java.lang.Thread.stop(Thread.java:853)
    6. at part_b_inside.chapter2_thread_life.terminal_thread.ThreadStopExample.main(ThreadStopExample.java:20)

    可以看到,该方法仅仅执行到i=329就结束了,后面的都还没有完成。如果这是一个线上的服务,例如正在处理账务等信息时就导致数据混乱,造成无法预知的问题,因此一定不能用stop()方法来中断线程。

    第三种:通过发送信号来终止线程

    其本质和开启类似,就是主线程给子线程发送一个可以关闭的信号,但是具体什么时候执行关闭由子线程决定。这就像你正在工作,女朋友突然打电话要你和她出去逛街,你说“稍等,我先将手上的工作完成”是一样的道理。也就是说main线程只给子线程发送信号来告知要结束,而不是暴力地直接将其停掉。具体是否要关闭由子线程根据自身状态决定是否停止。

    那通过信号停止线程,具体工作是怎么样的呢?应用程序发送一个线程终止的信号给JVM,JVM处理之后转给操作系统,操作系统再转给CPU,CPU收到之后会自行决定是否终止,而不一定马上终止。CPU此时可能在执行某个原子操作,或者要完成finally的功能才终止操作等,也就是会等手头的工作完成再终止(也叫安全点 ,或者安全区域)。

    在Java中,主要是通过interrupt和isInterruptted()。

    在Thread中提供了一个interrupt()方法,从名字看表示中断,但实际上并不像stop()方法一样直接中断线程,而是向子线程发送一个中断的通知。例如,假如你是领导,对于在加班的同事,你会说”做完就下班吧,其他明天再说“。这就是你给他发的信号量,而不是强制让他走,同事可以根据自己的情况处理完再走,这个时间可能是一分钟,也可能是一小时,决定权在同事这里。这就是信号量的含义,也是线程安全中断的基本模型。

    与interrupt()相配合的就是isInterruptted(),功能是判断是否收到了可以中断的请求。例如有的人一下午就看着领导走没走, 只要一走,立马开溜,这就是一直在通过isInterruptted()监听是否可以中断。

    接下来,我们通过代码看一下上述过程:

    1. public class InterruptDemo implements Runnable {
    2.   @Override
    3.   public void run() {
    4.       while (true) {
    5.         //执行操作  
    6.       }
    7.   }
    8. }

    很明显,上面的while无法结束。所以这里要将true改成能够判断当前线程的某个标记位,如果满足要求再继续循环,也就是这个代码:

    1. public class InterruptDemo implements Runnable {
    2.   @Override
    3.   public void run() {
    4.       //isInterrupted表示一个中断标记,默认是false
    5.       while (!Thread.currentThread().isInterrupted()) {
    6.         //执行操作
    7.       }
    8.   }
    9. }

    这样外部就可以通过设置该变量来终止了,像这个样子:

    1. public class InterruptDemo implements Runnable {
    2.   @Override
    3.   public void run() {
    4.       while (!Thread.currentThread().isInterrupted()) {
    5.         //执行操作
    6.       }
    7.   }
    8.   public static void main(String[] args) {
    9.       Thread thread=new Thread(new InterruptDemo());
    10.       thread.start();
    11.       //这里将共享变量isInterrupted设置为true,从而终止子线程
    12.       thread.interrupt();
    13.   }
    14. }

    具体是怎么实现的呢?很遗憾,两个都是native方法,不能直接看:

    1. private native void interrupt0();
    2. private native boolean isInterrupted(boolean ClearInterrupted);

    结论

    对于正在执行的线程,如何优雅地将其终止的: main线程无法确定子线程的当前状态,可以通过interrupt()指令来给其他线性发送终止信号,而接收方通过isInterrupted()来监听是否可以终止线程,收到信号之后可以自行决定是否终止。

    那native方法具体是咋回事呢,我们后面讲解Unsafe时再看。

    2 如何终止被阻塞的线程

    如果线程是sleep,join和waiting等状态,此时没有获得CPU时间片,也就无法及时感知到isInterrupted状态的变化,此时该如何中断呢?例如,假如某个人正在睡觉,如果发了通知,他根本不知道,例如这个代码:

    1. public class InterruptDemo2 implements Runnable {
    2.   @Override
    3.   public void run() {
    4.       try {
    5.           TimeUnit.SECONDS.sleep(20000000);
    6.       } catch (InterruptedException e) {
    7.           e.printStackTrace();
    8.       }
    9.   }
    10.     public static void main(String[] args) throws InterruptedException {
    11.       Thread t1 = new Thread(new InterruptDemo2());
    12.       t1.start();
    13.       Thread.sleep(1000);
    14.       t1.interrupt();
    15.   }
    16. }

    我们可以发现上面的代码是可以停止的,但是为什么所有睡眠的代码,都要求必须处理InterruptedException异常呢?就是为了利用异常机制来唤醒阻塞的线程的。

    这说明虽然线程是阻塞模式,但是触发异常之后就能获得CPU时间片来响应中断,并终止。 假如代码块在一个while自旋中,如何停止呢? 看例子:

    1. public class InterruptDemo02 implements Runnable{
    2.   @Override
    3.   public void run() {
    4.       while(!Thread.currentThread().isInterrupted()){ //①默认false,不做处理时中断之后会线程还挂着,不会退出
    5.           try {
    6.               TimeUnit.SECONDS.sleep(200);
    7.               System.out.println("run .....");
    8.           } catch (InterruptedException e) {
    9.               e.printStackTrace();
    10.           }
    11.       }
    12.       System.out.println("processor End");
    13.   }
    14.   public static void main(String[] args) throws InterruptedException {
    15.       Thread t1=new Thread(new InterruptDemo02());
    16.       t1.start();
    17.       Thread.sleep(100);
    18.       t1.interrupt();  
    19.   }
    20. }

    这个例子可以看到,抛出了sleep interrupt的异常,但是子线程并没有终止。

    运行到100ms时,主线程触发了子线程的interrupt exception,这个异常会触发线程复位。

    看下面的例子,特别是注释的内容:本来在子线程中Thread.currentThread().isInterrupted()是false的,当主线程执行t1.interrupt();时将其改成了true①。 子线程中的异常再次将Thread.currentThread().isInterrupted()改成了false②,所以子线程会继续循环,而不会停止。 如果要终止,③需要catch里再设置一次中断,也就是这样:

    1. public void run() {
    2.   while (!Thread.currentThread().isInterrupted()) { //①默认false,不做处理时中断之后会线程还挂着,不会退出
    3.       try {
    4.           TimeUnit.SECONDS.sleep(200);
    5.           System.out.println("run .....");
    6.       } catch (InterruptedException e) {//②,这里会将isInterrupted再改成true
    7.           e.printStackTrace();
    8.           System.out.println("interrupt...");
    9.           Thread.currentThread().interrupt();//③子线程自己设置一次中断,此时子线程才会真正中断
    10.       }
    11.   }
    12.   System.out.println("processor End");
    13. }

    执行结果是:

    可以看到打印了,线程也停止了。

    通过上面的代码我们可以看到,对于涉及线程阻塞的方法,例如Thread.join(),Thread.wait(),Thread.sleep()等等,都会抛出异常。之所以如此,是因为如果需要让一个处于阻塞的线程被中断,从而做出响应。

    我们再强调一下,主线程想让子线程停止要通过interrupt给子线程发一个信号,告诉要中断了,而不是强制将其中断。触发复位是为了让子线程保持原来的状态,是否中断则有子线程在catch中决定。如果不处理就不中断,如果要中断就是再执行一次Thread.currentThread().interrupt();

    上面这个逻辑可以这么想象:放假的早上你正在睡懒觉,你妈叫你起来吃饭(给子线程发一个中断睡眠的状态),你被临时叫醒了,告诉她你知道啦(子线程的中断被临时打破,并且给主线程一个响应,异常就是为了干这个的,而不是出错了),如果不给她回话,她可能会一直叫你,甚至砸你的门。但是决定权在你这里,如果你不搭理还会继续睡(睡眠复位),你还可以决定那我起床吧(再次给自己一个不睡眠信号),然后起床(真正执行中断睡眠,开始起床的操作)。 如果看图就这样子:

      main线程通过interrupt()通过修改子线程的共享变量isInterrupted状态来告诉子线程要终止,但是自己并不知道子线程当前的状态,也不能让子线程强制终止。子线程根据这个变量来判断自己是否应该终止。

    3.总结

    本节我们介绍了线程的几个基础问题,例如如何创建线程,生命周期是怎么样的。其中几个重点我们务必理解清楚:

    1.开启线程为什么要用start(),用户线程、JVM、操作系统和CPU分别做了什么。

    2.线程的生命周期有几种状态,每种状态的特征是什么,sleep、waiting和time_waiting有什么区别和联系。

    3.如何优雅的终止线程?此时用户线程、JVM、操作系统和CPU又分别做了什么。

    4.如何终止正在阻塞的线程?此时子线程和主线程是如何交互的。

  • 相关阅读:
    QGIS制作精美地图
    短信验证码服务
    ComfyUI搭建
    上采样,下采样,卷积,反卷积,池化,反池化,双线性插值【基本概念分析】
    【经历】跨境电商公司目前已在职近2年->丰富且珍贵
    使用fontforge修改字体,只保留数字
    PLC开放式以太网通信网络状态查看工具netstat
    【学习笔记】计算几何
    全网最牛自动化测试框架系列之pytest(10)-常用执行参数说明
    Vue数据代理
  • 原文地址:https://blog.csdn.net/xueyushenzhou/article/details/126497341