如何优雅的 kill 线程
kill 掉一个线程,感觉是一件很简单的事情,比如 JAVA 中为我们提供了 stop 方法可以立即终止线程的执行,达到 kill 掉线程的目的。
但实际上对线程的操作是一件精细活,对于一段正在执行的任务,我们不能只是简单粗暴的勒令其停止。原因就是,线程与资源是有关联的。
比如,一个线程持有某个 lock ,我们在线程释放 lock 前粗暴的停止了它的运行,那么可能导致其持有的 lock 永远不能被释放,等待在该 lock 上的其它线程也会永远的阻塞在该处永远无法继续执行。
再比如,一个线程正在进行 IO 操作,占用着硬盘中的某个文件,我们粗暴的终止了该线程,则这个文件便得不到释放,会一直处于被占用状态。
进程是资源分配单位,线程是运行调度单位。但这并不意味着,线程可以不考虑资源的调度随意的执行任务。恰恰相反的是,许多由线程发起申请得到的资源,只有发起申请的线程才能正确的释放它们。因为即使其对同一进程中的其它线程可见,其它线程也并不知道释放该资源的正确时机。
所以我们在终止线程时,不能采用类似 stop 这种简单粗暴的方式。应该确保在终止线程时,线程并不是立即被终止,而是将控制权交给该线程,由该线程自行判断应该做好哪些善后工作,做完这些善后工作后再停止。
JAVA 中的 interrupt 信号可以做到这一点,一个典型的安全终止线程的方式便是基于 interrupt 信号通知线程,以及通过 InterruptedException 异常回滚堆栈钩出阻塞中的线程,使其停止。
关于 JVM 是如何响应 interrupt 信号并抛出 InterruptedException 的,前面的博客有非常全面的解释:https://www.cnblogs.com/niuyourou/p/12392942.html。
我们来看一个 interrupt 终止线程的例子,一个典型的两步终止:
public class ThreadInterrupt extends Thread{ @Override public void run(){ int i=0; try { while (!Thread.currentThread().isInterrupted()) { System.out.println("线程进行第 " + i + " 次工作,此时 inerrupt 标志位为:"+Thread.currentThread().isInterrupted()); Thread.sleep(100); i++; } //这里可以进行善后工作,比如释放资源 System.out.println("线程接收到 interrupt 信号,离开工作区.此时 inerrupt 标志位为:"+Thread.currentThread().isInterrupted()); }catch (InterruptedException inE){ //这里可以进行善后工作,比如释放资源 System.out.println("线程在 sleep 的过程中被 interrupted,此时 interrupt 标志位为:"+Thread.currentThread().isInterrupted()); } } }
线程每执行完一次任务,都会检查一下是否有 interrupt 信号,如果有则进行善后工作并退出线程。
同时线程对 InterruptedException 进行了捕获,如果在 sleep/wait/join 过程中收到 interrupt 信号,则回滚堆栈到 try 处,进入 catch 块进行异常的处理,我们可以在catch 块中进行善后工作。
这样无论是运行状态还是阻塞状态(因为 sleep/wait/join 陷入的阻塞,因为我们只有通过这些方法可以使线程陷入阻塞。synchronized 等方法阻塞或唤醒线程是由 JVM 控制的,由 JVM 保证其正确性。)下的线程,都可以及时的响应我们的 interrupt 信号。
我们进行一下测试:
public class Test { public static void main(String[] args) throws InterruptedException{ Thread test1=new ThreadInterrupt(); test1.start(); Thread.sleep(1000); test1.interrupt(); } }
看一下效果,在 sleep 过程中接收到了 interrupt 信号:
在正常工作时接收到了 interrupt 信号:
我们可以看到,两次被 interrupt 线程表现并不同。
sleep 情况下进入 catch 块后,interrupt 状态被重置为了 false,而正常状态下被中断时 interrupt 状态未被重置,依然是true。
因为 interrupt 的本质是一个信号,正常情况下我们对一个信号进行了处理应该将其移出信号队列(当然这里不是队列),避免对其重复处理。
sleep 抛出异常前 JVM 帮助我们做到了这一点,我们在对信号进行处理时,也应该手动将其重置。
isInterrupted 只能获取信号状态,不会重置它。interrupted 可以返回信号的当前状态,如果未 true,返回后会将其重置为 false 。我们可以使用 interrupted 方法代替 isInterrupted 方法:
public class ThreadInterrupt extends Thread{ @Override public void run(){ int i=0; try { while (!Thread.interrupted()) { System.out.println("线程进行第 " + i + " 次工作,此时 inerrupt 标志位为:"+Thread.currentThread().isInterrupted()); Thread.sleep(1); i++; } //这里可以进行善后工作,比如释放资源 System.out.println("线程接收到 interrupt 信号,离开工作区.此时 inerrupt 标志位为:"+Thread.currentThread().isInterrupted()); }catch (InterruptedException inE){ //这里可以进行善后工作,比如释放资源 System.out.println("线程在 sleep 的过程中被 interrupted,此时 interrupt 标志位为:"+Thread.currentThread().isInterrupted()); } } }
这样再来看效果:
可以看到,在响应了中断信号后,interrupted 方法帮我们将 interrupt 状态重置为了 false。
第 50 次工作时,检测到了 interrupt 信号,但线程还是将其执行完毕后才退出了工作区,这也证实了该方法的可靠性,使线程尽量安全的退出。