【Kill Thread Part.1-3】线程停止、中断最佳实践
【Kill Thread Part.1-3】线程停止、中断最佳实践
- 讲解原理
- 最佳实践:如何正确停止线程
- 停止线程的错误方法
- 重要函数的源码解析
- 常见的面试问题
一、涵盖内容
二、原理讲解
原理介绍:使用interrupt来通知,而不是强制
我们只能用interrupt来通知线程,你该中断了,但是并不能强制线程中断,是否中断的决定权,在线程自己的手里。
Java认为,被停止的线程本身要更清楚自身的工作和运行状态,而我们在外部强制中断的话,并不是非常了解被中断线程的各个状态。
三、最佳实践:如何正确停止线程
1、通常线程会在什么情况下停止
- run()方法运行完毕
- 有异常出现,并且方法中没有捕获异常,线程停止,线程的资源被JVM回收
2、正确的停止方法:interrupt
①通常线程会在什么情况下停止普通情况
测试代码
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();
}
}
②线程可能被阻塞
测试代码
/**
* 描述: 带有sleep的中断线程的写法
* 打印出100的倍数,完成任务之后,需要做一个等待,在等待的时候,线程被中断
*/
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();
}
}
运行结果
结果分析
- 在代码中有可以让线程阻塞的方法,比如sleep()。必然会需要我们处理InterruptedException这样的异常
③如果线程在每次迭代后都阻塞
测试代码
/**
* 描述: 如果在执行过程中,每次循环都会调用sleep或者wait等方法,那么不需要每次迭代都检查是否已中断
*/
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();
}
}
运行结果
结果分析
- 在每一次迭代,都会阻塞的时候,我们就没有必要在while循环中去判断是否有阻塞信号
- 因为在sleep()方法会有代码帮助我们响应这样的中断
③阻塞方法在while循环里面,try/catch也在while循环里面,没有报过循环
测试代码
/**
* 描述: 如果while里面放try/catch,会导致中断失效
*/
public class CantInterrupt {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
int num = 0;
while (num <= 10000) {
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();
}
}
运行结果
发现这个线程没有被中断,在抛出异常之后,还再继续执行。
结果分析
- 通过上述的代码方式,我们可以看出,循环终止的条件并没有改变,只是在一次循环中抛出了这个异常,所以线程没有被中断。
- 如果在while循环的判断条件中加上是否已经停止的判断,仍然无法中断线程,这是为什么呢?
- 这是因为,在sleep()方法结束之后,会把线程的interrupted标志位清除,所以下一次循环判断的时候,标志位还是为0,则可以继续正常运行。
3、实际开发中的两种最佳实践
①优先选择:传递中断
糟糕的情况
/**
* 描述; 最佳实践:catch了InterruptedException之后
* 优先选择在方法签名中抛出异常
* 那么run()方法就会强制try/catch
*/
public class RightWayStopThreadInProd implements Runnable{
@Override
public void run() {
//即使这里做了判断,但是在sleep()之后,会把interrupted标志位擦除。
while (true && !Thread.currentThread().isInterrupted()) {
System.out.println("go");
throwInMethod();
}
}
private void throwInMethod() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new RightWayStopThreadInProd());
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
}
在这种情况下,我们的业务逻辑都在run()方法中,但是我们调用了一个其他小伙伴写的方法,这时候有一个线程想要中断我们这个线程,但是我们写的代码却无法处理和响应中断,这就是比较致命了。
最佳实践
注意,run方法无法再向上抛出异常,因为run方法是一个重写的方法,是Runnable接口定义好的
由于定义的时候就没有抛出,所以此时不允许我们再往上抛异常。在run方法里只能够try/catch
最佳实践处理代码
/**
* 描述; 最佳实践:catch了InterruptedException之后
* 优先选择在方法签名中抛出异常
* 那么run()方法就会强制try/catch
*/
public class RightWayStopThreadInProd implements Runnable{
@Override
public void run() {
while (true) {
System.out.println("go");
try {
throwInMethod();
} catch (InterruptedException e) {
//保存日志、停止程序等操作来响应中断
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();
}
}
②不想或无法传递:恢复中断
测试代码
/**
* 描述: 最佳实践2:在catch子语句中调用Thread.currentThread().interrupt()来恢复设置中断状态,
* 以便于在后续的执行中,依然能够检查到刚才发生了中断
* 回到刚才的RightWayStopThreadInProd补上中断,让它跳出
*/
public class RightWayStopThreadInProd2 implements Runnable{
@Override
public void run() {
while (true) {
System.out.println("go");
if (Thread.currentThread().isInterrupted()) {
System.out.println("线程中断!Interrupted,程序运行结束");
//保存日志、停止程序等操作来响应中断
System.out.println("保存日志!");
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();
}
}
运行结果
结果分析
以上做法说明,我们如果想到在别的方法中处理中断信号,还需要重新抛出中断信号,不应该把中断信号自己独吞。
③不应屏蔽中断
屏蔽中断:
- 即不在方法签名中抛出,也不在catch语句中恢复中断。
- 如果被调用的方法屏蔽了中断,会造成信息传递的不畅通。
4、会响应中断的方法总结列表
- 前三个方法说的是,这三个方法执行的过程当中,是能够感知到中断信号的。
除此之外还有下列的方法:
补充:Java异常体系
- Error指的是程序内部错误或者资源耗尽,意味着我们在代码层面无法catch这些错误。
- Exception发生了RuntimeException一般都是程序员自己的问题(程序问题)称为非受检查异常,也就是unchecked Exception
- 出了RuntimeException之外的其他Exception,都是受检查异常,可以抛出或者检测的,可以在程序中提前处理。提高健壮性。
四、停止线程的错误方法
1、被弃用的stop、suspend和resume方法
测试代码
/**
* 描述; 错误的停止方法:用stop()来停止线程,会导致线程运行一半,突然停止,没办法完成一个基本单位的操作
* 一个连队发弹药的例子,在这种情况下,会造成脏数据(有的连队会多领取少领取装备)
*/
public class StopThread implements Runnable{
@Override
public void run() {
//模拟指挥军队: 一共有5个连队,每个连队10人,以连队为单位发放武器弹药
//叫到号的士兵前去领取
for (int i = 0; i < 5; i++) {
System.out.println("连队" + i + "开始领取武器");
for (int j = 0; j < 10; j++) {
System.out.println(j);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("连队"+ i + "已经领取完毕");
}
}
public static void main(String[] args) {
Thread thread = new Thread(new StopThread());
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.stop();
}
}
运行结果
- 发生了数据错乱,如果我们是银行转账的线程,则会发生很大的问题,造成严重的后果,涉及到这样的问题,需要我们至少让这个线程把业务逻辑的最小执行单元运行完毕,然后再中断。
- suspend会把线程挂起,是带着锁休息的,容易造成线程发生死锁。resume方法会唤醒挂起的方法。
2、用volatile设置boolean标记位
①看上去可行
可行的代码
/**
* 描述:演示用volatile的局限:part1 看似可行
*/
public class WrongWayVolatile implements Runnable{
//多个线程可以对这个变量具有可见性
private volatile boolean canceled = false;
@Override
public void run() {
int num = 0;
try {
while (num <= 100000 && !canceled) {
if (num % 100 == 0) {
System.out.println(num + "是100的倍数");
}
num++;
Thread.sleep(1);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
WrongWayVolatile r = new WrongWayVolatile();
Thread thread = new Thread(r);
thread.start();
Thread.sleep(5000);
r.canceled = true;
}
}
程序运行之后,过了五秒钟确实是停止了。
②错误原因
测试代码
/**
* 描述: 演示用volatile的局限part2 陷入阻塞时,volatile是无法线程的 此例中,生产者的生产速度很快,消费者消费速度慢,所以阻塞队列满了以后,生产者会阻塞,等待消费者进一步消费
*/
public class WrongWayVolatileCantStop {
public static void main(String[] args) throws InterruptedException {
ArrayBlockingQueue storage = new ArrayBlockingQueue(10);
Producer producer = new Producer(storage);
Thread producerThread = new Thread(producer);
producerThread.start();
Thread.sleep(1000);
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);
}
}
class Producer implements Runnable {
public volatile boolean canceled = false;
BlockingQueue storage;
public Producer(BlockingQueue storage) {
this.storage = storage;
}
@Override
public void run() {
int num = 0;
try {
while (num <= 100000 && !canceled) {
if (num % 100 == 0) {
storage.put(num);
System.out.println(num + "是100的倍数,被放到仓库中了。");
}
num++;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("生产者结束运行");
}
}
}
class Consumer {
BlockingQueue storage;
public Consumer(BlockingQueue storage) {
this.storage = storage;
}
public boolean needMoreNums() {
if (Math.random() > 0.95) {
return false;
}
return true;
}
}
通过生产者消费者的案例,现实中,生产者的生产速度是很快的,但是消费者的消费速度很慢,因为要进行一些数据处理等等。
运行结果
消费者不需要数据了,但是我们的线程还没有停止。
错误原因分析
- 失效发生的点是产生阻塞
- 所以根本没有捕获到中断的信号
- 这就是我们要使用interrupted的原因,因为Java设计者已经考虑到了sleep(),wait()这些阻塞方法对于中断的影响。所以当我们使用interrupt方法的时候,就可以通知线程去中断。
③修正方式
测试代码
/**
* 描述: 用中断来修复刚才的无尽等待问题
*/
public class WrongWayVolatileFixed {
public static void main(String[] args) throws InterruptedException {
WrongWayVolatileFixed body = new WrongWayVolatileFixed();
ArrayBlockingQueue storage = new ArrayBlockingQueue(10);
Producer producer = body.new Producer(storage);
Thread producerThread = new Thread(producer);
producerThread.start();
Thread.sleep(1000);
Consumer consumer = body.new Consumer(storage);
while (consumer.needMoreNums()) {
System.out.println(consumer.storage.take() + "被消费了");
Thread.sleep(100);
}
System.out.println("消费者不需要更多数据了。");
producerThread.interrupt();
}
class Producer implements Runnable {
BlockingQueue storage;
public Producer(BlockingQueue storage) {
this.storage = storage;
}
@Override
public void run() {
int num = 0;
try {
//检查是否被中断了
while (num <= 100000 && !Thread.currentThread().isInterrupted()) {
if (num % 100 == 0) {
storage.put(num);
System.out.println(num + "是100的倍数,被放到仓库中了。");
}
num++;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("生产者结束运行");
}
}
}
class Consumer {
BlockingQueue storage;
public Consumer(BlockingQueue storage) {
this.storage = storage;
}
public boolean needMoreNums() {
if (Math.random() > 0.95) {
return false;
}
return true;
}
}
}
五、停止线程相关重要函数解析
1、interrupt方法
①Java源码
已经深入到了native方法,我们该怎么去看C++代码呢?
②interrupt0 C++代码分析
-
进入github找到openjkd
-
https://github.com/openjdk-mirror/jdk7u-jdk/find/master
-
点进去
找到了interrupt0在JVM中的名字
-
找jvm的实现
-
https://github.com/openjdk-mirror/jdk7u-hotspot/search?q=JVM_Interrupt
-
找到jvm.cpp这个类
-
继续在这个类中找到函数
-
继续往下找Thread.cpp
- 找到了对应的方法,发现调用了os::interrupt()
③os::interrupt方法
2、判断是否已经被中断的相关方法
①static boolean interrupted() --- 检测线程是否被中断
- 此方法返回布尔值之后,会把中断的状态直接设置为false。直接把线程的中断状态interrupted标志位清除了。
- 这个方法也是唯一能够清除线程中断标志位的办法。
源码分析:
②boolean isInterrupted() --- 检测线程是否被中断
这个方法不清除标志位。
③Thread.interrupted()的目的对象
注意:Thread.interrupted()方法的目标对象是“当前线程”,而不管本方法来自于那个对象。
测试代码
/**
* 描述: 注意Thread.interrupted()方法的目标对象是“当前线程”,而不管本方法来自于哪个对象
*/
public class RightWayInterrupted {
public static void main(String[] args) throws InterruptedException {
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
for (; ; ) {
}
}
});
// 启动线程
threadOne.start();
//设置中断标志
threadOne.interrupt();
//获取中断标志
System.out.println("isInterrupted: " + threadOne.isInterrupted());
//获取中断标志并重置
System.out.println("isInterrupted: " + threadOne.interrupted());
//获取中断标志并重直
System.out.println("isInterrupted: " + Thread.interrupted());
//获取中断标志
System.out.println("isInterrupted: " + threadOne.isInterrupted());
threadOne.join();
System.out.println("Main thread is over.");
}
}
运行结果
结果分析
第二个和第三个中断:threadOne.interrupted()执行这个的时main线程,main线程没有被中断所以返回false
六、面试常见问题
1、如何停止线程
2、如何处理不可中断的阻塞
- 特定情况特定的方法,尽可能的让它响应中断。