一、什么时候我们需要中断一个线程
在实际的开发中,有很多场景需要我们中断一个正在运行的线程,就比如:
当我们使用抢票软件时,其中某一个通道已经抢到了火车票,这个时候我们就需要通知其他线程停止工作。
当我们希望在一个限定的时间里获得任务结果的时候,也需要在任务超时的时候关闭它
但是Java并不能像代码块中的break一样干脆的退出线程运行,Java本身提供的API方法总是让人觉得差强人意,没有办法完美的解决我们的需求,那么我们该如何优雅的停止一个线程呢?
二、如何终止一个线程
1) 暴力停止
1. 使用stop方法停止线程
Java在停止线程的运行方面,提供了一个stop的方法,首先我们写段代码,演示stop方法是如何终止线程的:
package xiao.thread.stop;
public class StopThread {
private static int count;
public static void main(String[] args) throws InterruptedException {
// first :create a thread task
Thread thread = new Thread(() -> {
while (true) {
++count;
// 为了减少打印数 增加个计数判断
//也可调用sleep方法进行休眠
if (count % 100 == 0) {
System.out.println("此时线程依旧存活" + count);
}
}
}, "thread_task for stopThread");
thread.start();
// 如果直接终止 很有可能thread的run方法还没有开始执行 所以建议让main线程休眠一段时间来观察效果
Thread.currentThread().sleep(1_000);
thread.stop();
}
}
首先我们可以明显的观察到,当我们调用stop的时候,线程被暴力停止了。这种方式虽然简单,但是java语言本身并不提倡我们这么做。stop()方法呈删除线状态,已经表明了这个方法已经被弃用。
2. 为什么不提倡使用stop方法
其实不提倡使用stop方法的原因很简单,因为线程最优状态的终止是只能自杀而不能被杀。具体的来说就是:当我们通过其他线程调用stop方法时,此刻我们并不知道被杀死的线程执行到了哪里。就比如我们在做一个为集合添加数据的操作,我们此时无法知道数据的添加进行到了哪一步。而当我们调用stop方法,此时被杀死的线程会立即释放自身持有的锁,其他线程此时就可以看到未被处理完的数据,造成线程安全的问题,破坏了对象的一致性。
2) 捕获异常法
1. 中断机制
中断很好理解,它本身其实并不具备中断的能力,只是一种协作机制。在java中并没有对中断进行任何语法层面的实现,只能够靠我们利用已有的中断标识来记性业务处理达到中断的目的。
2. 相关API
在实现中断功能时,我们常用的API主要是三个方法:
1. public void interrupt
调用此方法会将线程的中断标识设为true2. public boolean isInterrupt
调用此方法将会返回此刻现成的中断标识3.public static boolean interruped
该方法只能通过Thread.interrupted()调用,他会做两个操作
第一步返回当前线程的中断状态
第二部将当前现成的中断标识设为false
3. 证实interrupt没有停止线程的能力
首先我们举个小例子来证明当我们调用interrupt方法时,jvm无法为我们中断目标线程:
public class InterruptThread {
private static int count;
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while (true) {
System.out.println("线程依旧存活:_此时计数器状态为_" + ++count);
try {
Thread.currentThread().sleep(1_000);
} catch (InterruptedException e) {
}
}
},"测试线程");
thread.start();
thread.interrupt();
}
}
我们看下输出结果:
虽然我们在调用start方法后立即调用了interrupt,但是目标的测试线程依旧每隔一秒在控制台打印了计数器状态,并没有实际的中断线程,通过这个例子我们证明了在我们调用interrupt的时候,jvm并没有立即为我们停止目标线程的运行,如果我们想要在调用interrupt之后停止线程该如何做呢?
4. 异常处理法停止线程
既然我们证实了jvm确实不会主动地替我们中断线程,那么我们就需要利用interrupt方法中提到的中断标识来做一些事情。首先我们需要证明一个新的论点:
当阻塞方法接收到中断信号的时候,会抛出一个InterruptedException 异常
那么我们改造下上面的代码:
public class InterruptThread {
private static int count;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (true) {
System.out.println(Thread.currentThread().getName()+"正在运行~");
try {
Thread.currentThread().sleep(2_500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
System.out.println(Thread.currentThread().getName()+"接收到中断异常");
}
}
}, "测试线程");
thread.start();
Thread.currentThread().sleep(1_000);
thread.interrupt();
}
}
运行结果:
在这里我们可以看出,当我们调用interrupt方法时,由于线程此时也调用了可能将线程阻塞的方法sleep,因此此时目标线程在收到我们的中断信号的时候抛出了interruptException,但是由于while方法体里面还有代码需要执行,线程此时并没有结束,这个时候我们就需要利用我们捕获到的异常改造下代码:
public class InterruptThread {
private static int count;
private static boolean mark = true ;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (mark) {
System.out.println(Thread.currentThread().getName()+"正在运行~");
try {
Thread.currentThread().sleep(2_500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
mark = false ;
System.err.println(Thread.currentThread().getName()+"接收到中断信号");
}
}
}, "测试线程");
thread.start();
Thread.currentThread().sleep(1_000);
thread.interrupt();
}
}
运行结果:
可以看出程序在接收到我们的中断信号后就没有继续运行,线程停止。而上面代码的变化其实就发生在while的判断条件上,我们加入了一个布尔变量mark,通过捕获异常停止线程的原理其实也很简单:我们认为的设置一个中断变量,将该值设置为代码运行的入口条件,当我们捕获到异常的时候,改变中断变量的值以达到跳出循环的目的从而实现停止线程。
当然,实现跳出循环的方式有很多,我们也可以通过return关键字实现跳出循环的效果:
public class InterruptThread {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (true) {
System.out.println(Thread.currentThread().getName()+"正在运行~");
try {
Thread.currentThread().sleep(2_500);
} catch (InterruptedException e) {
e.printStackTrace();
System.err.println(Thread.currentThread().getName()+"接收到中断信号");
return ;
}
}
}, "测试线程");
thread.start();
Thread.currentThread().sleep(1_000);
thread.interrupt();
}
}
运行结果:
通过return也可以很好地利用异常来结束线程的运行。
当然,我们还有个关于interruptException异常的问题要说下
在实际的开发中,我们不能不管不顾的通过抛出异常的方式来结束线程,如果你只是想记录日志,那么此时应该将中断标记重新置为false,以避免程序意外中断
3) 通过守护线程实现(了解即可)
我们也可以利用守护线程的特性来结束一个线程的运行
如果不了解守护线程的特性,可以看我之前的文章
线程的简介
下面我们先看代码实现:
public class DaemonThreadInterrupt {
public static Thread thread;
public boolean flag = true;
public void execut(Runnable task) {
thread = new Thread(()->{
Thread thread2 = new Thread(task,"任务线程");
thread2.setDaemon(true);
thread2.start();
try {
thread2.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
},"执行线程");
thread.start();
}
public void stop() {
thread.interrupt();
}
public static void main(String[] args) {
DaemonThreadInterrupt daemonThreadInterrupt = new DaemonThreadInterrupt();
daemonThreadInterrupt.execut(()->{
while(true) {
System.out.println("程序运行");
try {
thread.sleep(1_000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
daemonThreadInterrupt.stop();
}
}
实现原理:
在上述代码中,我将需要实现的任务线程放到了一个执行线程中,执行线程不负责任何业务逻辑的处理,只负责启动任务线程。而任务线程在启动后,将他设为了执行线程的守护线程。然后让他join到执行线程中。
然后接下来我们就利用了join方法的特性以及守护线程的特点设计了stop方法:
执行join方法后,执行线程将等待任务线程执行完毕后才会执行,但是当我们调用interrupt方法时,join方法接收到中断信号就会抛出异常,此时执行线程不在等待任务线程的运行。重点来了:此时执行线程执行完毕,任务线程作为执行线程的守护线程也结束了他的生命周期。
这种方式充分利用了线程的多种特性停止了一个线程,而且结束的过程可控,也可设置延时结束,适合用作定时任务线程的实现。