如何使用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://abwzjs.com/article/gogdse.html