如何正确的停止线程
如何正确的停止线程
1.原理介绍
通常情况下,我们不会手动停止一个线程,而是允许线程运行到结束,然后让它自然停止。但是依然会
有许多特殊的情况需要我们提前停止线程,比如:用户突然关闭程序,或程序运行出错重启等。
在这种情况下,即将停止的线程在很多业务场景下仍然很有价值。尤其是我们想写一个健壮性很好,能
够安全应对各种场景的程序时,正确停止线程就显得格外重要。但是 Java 并没有提供简单易用,能够
直接安全停止线程的能力。
**对于 Java 而言,最正确的停止线程的方式是使用 interrupt。**但 interrupt 仅仅起到通知被停止线程的
作用。而对于被停止的线程而言,它拥有完全的自主权,它既可以选择立即停止,也可以选择一段时间
后停止,也可以选择压根不停止。那么为什么 Java 不提供强制停止线程的能力呢?Java 希望程序间能够相互通知、相互协作地管理线程,因为如果不了解对方正在做的工作,贸
然强制停止线程就可能会造成一些安全的问题,为了避免造成问题就需要给对方一定的时间来整理收尾工作。
2.如何用 interrupt 停止线程
while (!Thread.currentThread().isInterrupted() && more work to do) {
do more work
}
package com.moon.threads;
/**
* 线程停止
*/
public class StopThread implements Runnable {
@Override
public void run() {
int num =0;
while (!Thread.currentThread().isInterrupted()&& num<5){
num++;
System.out.printf("num=="+num);
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new StopThread());
thread.start();
Thread.sleep(2);
thread.interrupt();
}
}
执行结果如下:
num==1
Process finished with exit code 0
这个示例用线程的interrupt,优雅的停止了线程。这个示例的目的是打印输出1-5,当线程休眠2ms后,执行interrupt,然后到第二次循环发现了中断表示,则停止了线程。
3.者休眠过长时间会导致interrupt失效,如下:
package com.moon.threads;
/**
* 线程停止
*/
public class SleepThreadTest implements Runnable {
@Override
public void run() {
int num =0;
while (!Thread.currentThread().isInterrupted()&& num<5){
num++;
System.out.printf("num=="+num);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new SleepThreadTest());
thread.start();
Thread.sleep(2);
thread.interrupt();
}
}
执行结果:
如果 sleep、wait 等可以让线程进入阻塞的方法使线程休眠了,而处于休眠中的线程被中断,那么线程
是可以感受到中断信号的,并且会抛出一个 InterruptedException 异常,同时清除中断信号,将中断
标记位设置成 false。这样一来就不用担心长时间休眠中线程感受不到中断了,因为即便线程还在休
眠,仍然能够响应中断通知,并抛出异常。
在实际开发中肯定是团队协作的,不同的人负责编写不同的方法,然后相互调用来实现整个业务的逻
辑。那么如果我们负责编写的方法需要被别人调用,同时我们的方法内调用了 sleep 或者 wait 等能响
应中断的方法时,仅仅 catch 住异常是不够的 。需要在异常catch中再次打断 从而真正停止线程。
如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zmDMwN3b-1669385950210)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20221125215403733.png)]
错误的停止方法
- stop()
- suspend() –导致死锁
- resume()–导致死锁
这些方法已经被 Java直接标记为 @Deprecated。是因为 stop() 会直接把线程停止,这样就没有给线程足够的时间来处理想要在停止前保存数据的逻辑,任务戛然而止,会导致出现数据完整性等问题。
假设线程 A 调用了 suspend() 方法让线程 B 挂起,线程 B 进入休眠,而线程 B 又刚好持有一把锁,此
时假设线程 A 想访问线程 B 持有的锁,但由于线程 B 并没有释放锁就进入休眠了,所以对于线程 A 而
言,此时拿不到锁,也会陷入阻塞,那么线程 A 和线程 B 就都无法继续向下执行。
volatile 修饰标记位适用的场景
package com.moon.threads;
/**
* Volatile 停止线程的正确方式
*/
public class VolatileCanStop implements Runnable{
private volatile boolean canceled = false;
@Override
public void run() {
int num = 0;
try {
while (!canceled && num <= 10000) {
if (num % 5 == 0) {
System.out.println(num + "是5的倍数。");
}
num++;
Thread.sleep(1);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
VolatileCanStop r = new VolatileCanStop();
Thread thread = new Thread(r);
thread.start();
Thread.sleep(1000);
r.canceled = true;
}
}
volatile 修饰标记位不适用的场景
接下来我们就用一个生产者 / 消费者模式的案例来演示为什么说 volatile 标记位的停止方法是不完美
的。下面来看下 main 函数,首先创建了生产者 / 消费者共用的仓库 BlockingQueue storage,仓库容量是
8,并且建立生产者并将生产者放入线程后启动线程,启动后进行 500 毫秒的休眠,休眠时间保障生产
者有足够的时间把仓库塞满,而仓库达到容量后就不会再继续往里塞,这时生产者会阻塞,500 毫秒后
消费者也被创建出来,并判断是否需要使用更多的数字,然后每次消费后休眠 100 毫秒,这样的业务逻
辑是有可能出现在实际生产中的。
当消费者不再需要数据,就会将 canceled 的标记位设置为 true,理论上此时生产者会跳出 while 循
环,并打印输出 “生产者运行结束”。
然而结果却不是我们想象的那样,尽管已经把 canceled 设置成 true,但生产者仍然没有停止,这是因
为在这种情况下,生产者在执行 storage.put(num) 时发生阻塞,在它被叫醒之前是没有办法进入下一
次循环判断 canceled 的值的,所以在这种情况下用 volatile 是没有办法让生产者停下来的,相反如果
用 interrupt 语句来中断,即使生产者处于阻塞状态,仍然能够感受到中断信号,并做响应处理。