网站建设资讯

NEWS

网站建设资讯

如何使用interrupt停止线程

本篇内容介绍了“如何使用interrupt停止线程”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!

网站建设哪家好,找创新互联建站!专注于网页设计、网站建设、微信开发、微信小程序开发、集团企业网站建设等服务项目。为回馈新老客户创新互联还提供了新河免费建站欢迎大家使用!

启动一个线程很简单,调用Thread类的start()方法,并在run()方法中定义需要执行的任务就可以了,但是要正确的停止停止一个线程就需要注意了

一:如何正确的停止线程

通常情况下,我们不会手动停止一个线程,而是允许线程运行到结束,然后让它自然停止。但是依然会有许多特殊的情况需要我们提前停止线程,比如:用户突然关闭程序,或程序运行出错重启等。尤其是即将停止的线程在业务场景下仍然很有价值,我们就更需要正确的停止线程了。但是Java中并没有直接提供能够简单安全的停止线程的能力。

1.1 线程停止设计原则

Java 希望程序间能够相互通知、相互协作地管理线程,因为如果不了解对方正在做的工作,贸然强制停止线程就可能会造成一些安全的问题,为了避免造成问题就需要给对方一定的时间来整理收尾工作。

比如:线程正在写入一个文件,这时收到终止信号,它就需要根据自身业务判断,是选择立即停止,还是将整个文件写入成功后停止,而如果选择立即停止就可能造成数据不完整,不管是中断命令发起者,还是接收者都不希望数据出现问题。

因此对于Java而言最正确的停止线程的方式是使用 interrupt。因为interrupt 仅仅起到通知被停止线程的作用。而对于被停止的线程而言,它拥有完全的自主权,它既可以选择立即停止,也可以选择一段时间后停止,也可以选择压根不停止。

1.2 如何使用interrupt停止线程

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

调用用某个线程的 interrupt() 方法之后,这个线程的中断标记位就会被设置成 true。每个线程都有这样的标记位,当线程执行时,应该定期检查这个标记位,如果标记位被设置成 true,就说明有程序想终止该线程。在上面伪代码中可以看到,在 while 循环体判断语句中,首先通过 Thread.currentThread().isInterrupt() 判断线程是否被中断,随后检查是否还有工作要做。

使用interrupt正确的停止线程:

public class StopThread implements Runnable{
    @Override
    public void run() {
        int count = 0;
        // 退出循环表示中断标志位被设置为了true(有人想停止线程)或任务完成
        while(!Thread.currentThread().isInterrupted() && count < 1000){
            // 中断标记为false(未改变)、且还有任务做则进入循环,每次都判断一下
            System.out.println("count = " + count++);
        }
    }

    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(new StopThread());
        thread.start();
        // 执行当前语句的线程进行睡眠
        Thread.sleep(5);
        thread.interrupt();
    }
}

在 main 函数中会启动该子线程,然后主线程休眠 5 毫秒后立刻给该子线程发送中断信号,该子线程会检测到中断信号,于是在还没打印完1000个数的时候就会停下来,这种就属于通过 interrupt 正确停止线程的情况。

1.2.1 线程在Sleep期间能否感受到interrupt中断

Java 设计者在设计之初就考虑到了这一点。sleep、wait 等可以让线程进入阻塞的方法使线程休眠了,而处于休眠中的线程被中断,那么线程是可以感受到中断信号的,并且会抛出一个 InterruptedException 异常,同时清除中断信号,将中断标记位设置成 false。这样一来就不用担心长时间休眠中线程感受不到中断了,因为即便线程还在休眠,仍然能够响应中断通知,并抛出异常。

public class StopDuringSleep implements Runnable{

    @Override
    public void run() {
        int num = 0;
        while(!Thread.currentThread().isInterrupted() && num <= 1000){
            try {
                System.out.println("num = "+num++);
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

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

上面代码的逻辑为:主线程休眠 5 毫秒后,通知子线程中断,此时子线程仍在执行 sleep 语句,处于休眠中。执行代码结果发现程序仍在继续执行,但抛出了InterruptedException异常信息。可以看出线程即使在在Sleep期间也能够感受到interrupt中断信息,程序仍在运行没有正确停止的原因是我们没有正确处理这个异常。

在实际开发中,如果我们负责编写的方法需要被别人调用,同时我们的方法内调用了 sleep 或者 wait 等能响应中断的方法时,仅仅 catch 住异常是不够的。如上面的代码所示,catch 语句块里代码是空的,它并没有进行任何处理。假设线程执行到这个方法,并且正在 sleep,此时有线程发送 interrupt 通知试图中断线程,就会立即抛出异常,并清除中断信号。抛出的异常被 catch 语句块捕捉。但是,捕捉到异常的 catch 没有进行任何处理逻辑,相当于把中断信号给隐藏了,这样做是非常不合理的,所以需要正确处理异常,两种最佳处理异常的方法如下:

1)方法签名抛异常,run() 强制 try/catch
void subTask2() throws InterruptedException {
    Thread.sleep(1000);
}

每一个方法的调用方有义务去处理异常。调用方要不使用 try/catch 并在 catch 中正确处理异常,要不将异常声明到方法签名中。如果每层逻辑都遵守规范,便可以将中断信号层层传递到顶层,最终让 run() 方法可以捕获到异常。而对于 run() 方法而言,它本身没有抛出 checkedException 的能力,只能通过 try/catch 来处理异常。层层传递异常的逻辑保障了异常不会被遗漏,而对 run() 方法而言,就可以根据不同的业务逻辑来进行相应的处理。

2)再次中断
private void reInterrupt() {
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        e.printStackTrace();
    }
}

在 catch 语句中再次中断线程。如代码所示,需要在 catch 语句块中调用 Thread.currentThread().interrupt() 函数。因为如果线程在休眠期间被中断,那么会自动清除中断信号。如果这时手动添加中断信号,中断信号依然可以被捕捉到。这样后续执行的方法依然可以检测到这里发生过中断,可以做出相应的处理,整个线程可以正常退出。

二:错误的停止线程的方法

2.1 常见的错误停止线程的方法

几种停止线程的错误方法。比如 stop(),suspend() 和 resume(),这些方法已经被 Java 直接标记为 @Deprecated。我们不应该再使用它们了。

  • stop():会直接把线程停止,这样就没有给线程足够的时间来处理想要在停止前保存数据的逻辑,任务戛然而止,会导致出现数据完整性等问题。

  • suspend() 和 resume() :它们的问题在于如果线程调用 suspend(),它并不会释放锁,就开始进入休眠,但此时有可能仍持有锁,这样就容易导致死锁问题,因为这把锁在线程被 resume() 之前,是不会被释放的。

例如:设线程 A 调用了 suspend() 方法让线程 B 挂起,线程 B 进入休眠,而线程 B 又刚好持有一把锁,此时假设线程 A 想访问线程 B 持有的锁,但由于线程 B 并没有释放锁就进入休眠了,所以对于线程 A 而言,此时拿不到锁,也会陷入阻塞,那么线程 A 和线程 B 就都无法继续向下执行。

2.2 为什么用 volatile 标记位的停止线程方法是错误的

volatile 修饰标记位适用的场景

public class VolatileCanStop implements Runnable{

    private volatile boolean canceled = false;

    @Override
    public void run() {
        int num = 0;
        try {
            while (!canceled && num <= 1000000) {
                if (num % 10 == 0) {
                    System.out.println(num + "是10的倍数。");
                }
                num++;
                Thread.sleep(1);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {

        VolatileCanStop volatileCanStop = new VolatileCanStop();
        Thread thread = new Thread(volatileCanStop);
        thread.start();
        Thread.sleep(300);
        volatileCanStop.canceled = true;
    }
}

run() 方法中进行 while 循环,在循环体中又进行了两层判断,首先判断 canceled 变量的值,canceled 变量是一个被 volatile 修饰的初始值为 false 的布尔值,当该值变为 true 时,while 跳出循环,while 的第二个判断条件是 num 值小于1000000(一百万),在while 循环体里,只要是 10 的倍数就打印出来,然后 num++。

接下来,首先启动线程,然后经过 3 秒钟的时间,把用 volatile 修饰的布尔值的标记位设置成 true,这样,正在运行的线程就会在下一次 while 循环中判断出 canceled 的值已经变成 true 了,这样就不再满足 while 的判断条件,跳出整个 while 循环,线程就停止了,这种情况是演示 volatile 修饰的标记位可以正常工作的情况,但是如果我们说某个方法是正确的,那么它应该不仅仅是在一种情况下适用,而在其他情况下也应该是适用的

volatile 修饰标记位不适用的场景

public class VolatileCannotStop {

   /**
    * 生产者
    */
   static class Producer implements Runnable{

       public volatile boolean canceled = false;
       BlockingQueue storage;

       public Producer(BlockingQueue storage) {
           this.storage = storage;
       }

       @Override
       public void run() {
           try{
               int num = 0;
               // 停止线程信号为true,且任务执行完毕
               while(num <= 100000 && !canceled){
                   if (num % 50 == 0) {
                       // 生产者队列满了之后,会使当前线程阻塞,阻塞后(被叫醒前)不能进入下一次循环,此时是感知不到canceled的状态的
                       storage.put(num);
                       System.out.println(num + "是50的倍数,被放到仓库中了。");
                   }
                   num++;
               }
           }catch (InterruptedException e){
               e.printStackTrace();
           }finally {
               System.out.println("生产者结束运行");
           }
       }
   }

   /**
    * 消费者
    */
   static class Consumer{

       BlockingQueue storage;

       public Consumer(BlockingQueue storage) {
           this.storage = storage;
       }
       public boolean needMoreNums() {
           if (Math.random() > 0.97) {
               return false;
           }
           return true;
       }
   }

   public static void main(String[] args) throws InterruptedException {

       ArrayBlockingQueue storage = new ArrayBlockingQueue(8);

       Producer producer = new Producer(storage);
       Thread producerThread = new Thread(producer);
       producerThread.start();
       Thread.sleep(500);

       Consumer consumer = new Consumer(storage);
       while (consumer.needMoreNums()) {
           System.out.println(consumer.storage.take() + "被消费了");
           Thread.sleep(100);
       }
       System.out.println("消费者不需要更多数据了。");

       //一旦消费不需要更多数据了,我们应该让生产者也停下来,但是实际情况却停不下来
       producer.canceled = true;
       System.out.println(producer.canceled);
   }
}

main 函数中,首先创建了生产者/消费者共用的仓库 BlockingQueue storage,仓库容量是 8,并且建立生产者并将生产者放入线程后启动线程,启动后进行 500 毫秒的休眠,休眠时间保障生产者有足够的时间把仓库塞满,而仓库达到容量后就不会再继续往里塞,这时生产者会阻塞,500 毫秒后消费者也被创建出来,并判断是否需要使用更多的数字,然后每次消费后休眠 100 毫秒。

当消费者不再需要数据,就会将 canceled 的标记位设置为 true,理论上此时生产者会跳出 while 循环,打印输出canceled的值为true,并打印输出“生产者运行结束”。

然而结果却不是我们想象的那样,尽管已经把 canceled 设置成 true,但生产者仍然没有停止,这是因为在这种情况下,生产者在执行 storage.put(num) 时发生阻塞,在它被叫醒之前是没有办法进入下一次循环判断 canceled 的值的,所以在这种情况下用 volatile 是没有办法让生产者停下来的,相反如果用 interrupt 语句来中断,即使生产者处于阻塞状态,仍然能够感受到中断信号,并做响应处理

“如何使用interrupt停止线程”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注创新互联网站,小编将为大家输出更多高质量的实用文章!


分享标题:如何使用interrupt停止线程
文章转载:http://cdweb.net/article/gogdse.html