线程之中,一个线程的生命周期分为:初始、就绪、运行、阻塞以及结束。当然,其中也可以有四种状态,初始、就绪、运行以及结束。
一般而言,可能有三种原因引起阻塞:等待阻塞、同步阻塞以及其他阻塞(睡眠、join或者IO阻塞);对于Java而言,等待阻塞是调用wait方法产生的,同步阻塞则是由同步块(synchronized)产生的,睡眠阻塞是由sleep产生的,join阻塞是由join方法产生的。
言归正传,要中断一个Java线程,可调用线程类(Thread)对象的实例方法:interrupte();然而interrupte()方法并不会立即执行中断操作;具体而言,这个方法只会给线程设置一个为true的中断标志(中断标志只是一个布尔类型的变量),而设置之后,则根据线程当前的状态进行不同的后续操作。如果,线程的当前状态处于非阻塞状态,那么仅仅是线程的中断标志被修改为true而已;如果线程的当前状态处于阻塞状态,那么在将中断标志设置为true后,还会有如下三种情况之一的操作:
- 如果是wait、sleep以及jion三个方法引起的阻塞,那么会将线程的中断标志重新设置为false,并抛出一个InterruptedException;
- 如果是java.nio.channels.InterruptibleChannel进行的io操作引起的阻塞,则会对线程抛出一个ClosedByInterruptedException;(待验证)
- 如果是轮询(java.nio.channels.Selectors)引起的线程阻塞,则立即返回,不会抛出异常。(待验证)
如果在中断时,线程正处于非阻塞状态,则将中断标志修改为true,而在此基础上,一旦进入阻塞状态,则按照阻塞状态的情况来进行处理;例如, 一个线程在运行状态中,其中断标志被设置为true,则此后,一旦线程调用了wait、join、sleep方法中的一种,立马抛出一个InterruptedException,且中断标志被清除,重新设置为false。
通过上面的分析,我们可以总结, 调用线程类的interrupted方法,其本质只是设置该线程的中断标志,将中断标志设置为true,并根据线程状态决定是否抛出异常。因此,通过interrupted方法真正实现线程的中断原理是: 开发人员根据中断标志的具体值,来决定如何退出线程。
一个简单的实现方式如下:
- public void run() {
- try {
- while (true){
- Thread.sleep(1000l);//阻塞状态,线程被调用了interrupte()方法,清除中断标志,抛出InterruptedException
- //dosomething
- boolean isIn = this.isInterrupted();
- //运行状态,线程被调用了interrupte()方法,中断标志被设置为true
- //非阻塞状态中进行中断线程操作
- if(isIn) break;//退出循环,中断进程
- }
- }catch (InterruptedException e){ //阻塞状态中进行中断线程操作
- boolean isIn = this.isInterrupted();//退出阻塞状态,且中断标志被清除,重新设置为false,所以此处的isIn为false
- return;//退出run方法,中断进程
- }
- }
分别考虑了阻塞状态中进行中断线程和非阻塞状态中中断线程的处理方式。
最后,说明一下interrupte方法的调用,该方法可在需要中断的线程本身中调用,也可在其他线程中调用需要中断的线程对象的该方法。
(一).关于interrupt()
interrupt()并不直接中断线程,而是设定一个中断标识,然后由程序进行中断检查,确定是否中断。
1. sleep() & interrupt() 线程A正在使用sleep()暂停着: Thread.sleep(100000); 如果要取消他的等待状态,可以在正在执行的线程里(比如这里是B)调用a.interrupt(); 令线程A放弃睡眠操作,这里a是线程A对应到的Thread实例执行interrupt()时,并不需要获取Thread实例的锁定.任何线程在任何时刻,都可以调用其他线程interrupt().当sleep中的线程被调用interrupt()时,就会放弃暂停的状态.并抛出InterruptedException.丢出异常的,是A线程.
2. wait() & interrupt() 线程A调用了wait()进入了等待状态,也可以用interrupt()取消. 不过这时候要小心锁定的问题.线程在进入等待区,会把锁定解除,当对等待中的线程调用interrupt()时(注意是等待的线程调用其自己的interrupt()),会先重新获取锁定,再抛出异常.在获取锁定之前,是无法抛出异常的.
3. join() & interrupt() 当线程以join()等待其他线程结束时,一样可以使用interrupt()取消之.因为调用join()不需要获取锁定,故与sleep()时一样,会马上跳到catch块里. 注意是随调用interrupt()方法,一定是阻塞的线程来调用其自己的interrupt方法.如在线程a中调用来线程t.join().则a会等t执行完后在执行t.join后的代码,当在线程b中调用来a.interrupt()方法,则会抛出InterruptedException
4. interrupt()只是改变中断状态而已 interrupt()不会中断一个正在运行的线程。这一方法实际上完成的是,在线程受到阻塞时抛出一个中断信号,这样线程就得以退出阻塞的状态。更确切的说,如果线程被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞,那么,它将接收到一个中断异常(InterruptedException),从而提早地终结被阻塞状态。 如果线程没有被阻塞,这时调用interrupt()将不起作用;否则,线程就将得到异常(该线程必须事先预备好处理此状况),接着逃离阻塞状态。 线程A在执行sleep,wait,join时,线程B调用A的interrupt方法,的确这一个时候A会有InterruptedException异常抛出来.但这其实是在sleep,wait,join这些方法内部会不断检查中断状态的值,而自己抛出的InterruptedException。 如果线程A正在执行一些指定的操作时如赋值,for,while,if,调用方法等,都不会去检查中断状态,所以线程A不会抛出InterruptedException,而会一直执行着自己的操作.当线程A终于执行到wait(),sleep(),join()时,才马上会抛出InterruptedException. 若没有调用sleep(),wait(),join()这些方法,或是没有在线程里自己检查中断状态自己抛出InterruptedException的话,那InterruptedException是不会被抛出来的.
(二)Java线程中断的本质和编程原则 在历史上,Java试图提供过抢占式限制中断,但问题多多,例如前文介绍的已被废弃的Thread.stop、Thread.suspend和 Thread.resume等。另一方面,出于Java应用代码的健壮性的考虑,降低了编程门槛,减少不清楚底层机制的程序员无意破坏系统的概率。
如今,Java的线程调度不提供抢占式中断,而采用协作式的中断。其实,协作式的中断,原理很简单,就是轮询某个表示中断的标记,我们在任何普通代码的中都可以实现。 例如下面的代码:
volatile bool isInterrupted;
//…
while(!isInterrupted) {
compute();
}
但是,上述的代码问题也很明显。当compute执行时间比较长时,中断无法及时被响应。另一方面,利用轮询检查标志变量的方式,想要中断wait和sleep等线程阻塞操作也束手无策。
如果仍然利用上面的思路,要想让中断及时被响应,必须在虚拟机底层进行线程调度的对标记变量进行检查。是的,JVM中确实是这样做的。下面摘自java.lang.Thread的源代码:
public static boolean interrupted() { return currentThread().isInterrupted(true); }
//…
private native boolean isInterrupted(boolean ClearInterrupted);
可以发现,isInterrupted被声明为native方法,取决于JVM底层的实现。
实际上,JVM内部确实为每个线程维护了一个中断标记。但应用程序不能直接访问这个中断变量,必须通过下面几个方法进行操作: public class Thread { //设置中断标记 public void interrupt() { ... } //获取中断标记的值 public boolean isInterrupted() { ... } //清除中断标记,并返回上一次中断标记的值 public static boolean interrupted() { ... } ... }
通常情况下,调用线程的interrupt方法,并不能立即引发中断,只是设置了JVM内部的中断标记。因此,通过检查中断标记,应用程序可以做一些特殊操作,也可以完全忽略中断。 你可能想,如果JVM只提供了这种简陋的中断机制,那和应用程序自己定义中断变量并轮询的方法相比,基本也没有什么优势。 JVM内部中断变量的主要优势,就是对于某些情况,提供了模拟自动“中断陷入”的机制。 在执行涉及线程调度的阻塞调用时(例如wait、sleep和join),如果发生中断,被阻塞线程会“尽可能快的”抛出InterruptedException。因此,我们就可以用下面的代码框架来处理线程阻塞中断: try { //wait、sleep或join
} catch(InterruptedException e) { //某些中断处理工作
} 所谓“尽可能快”,我猜测JVM就是在线程调度调度的间隙检查中断变量,速度取决于JVM的实现和硬件的性能。
然而,对于某些线程阻塞操作,JVM并不会自动抛出InterruptedException异常。例如,某些I/O操作和内部锁操作。对于这类操作,可以用其他方式模拟中断: 1)java.io中的异步socket I/O 读写socket的时候,InputStream和OutputStream的read和write方法会阻塞等待,但不会响应java中断。不过,调用Socket的close方法后,被阻塞线程会抛出SocketException异常。 2)利用Selector实现的异步I/O 如果线程被阻塞于Selector.select(在java.nio.channels中),调用wakeup方法会引起ClosedSelectorException异常。 3)锁获取 如果线程在等待获取一个内部锁,我们将无法中断它。但是,利用Lock类的lockInterruptibly方法,我们可以在等待锁的同时,提供中断能力。 另外,在任务与线程分离的框架中,任务通常并不知道自身会被哪个线程调用,也就不知道调用线程处理中断的策略。所以,在任务设置了线程中断标记后,并不能确保任务会被取消。因此,有以下两条编程原则: 1)除非你知道线程的中断策略,否则不应该中断它。 这条原则告诉我们,不应该直接调用Executer之类框架中线程的interrupt方法,应该利用诸如Future.cancel的方法来取消任务。 2)任务代码不该猜测中断对执行线程的含义。 这条原则告诉我们,一般代码遇在到InterruptedException异常时,不应该将其捕获后“吞掉”,而应该继续向上层代码抛出。 总之,Java中的非抢占式中断机制,要求我们必须改变传统的抢占式中断思路,在理解其本质的基础上,采用相应的原则和模式来编程。
(三) interrupt() 与 cancel()的区别
两者实际上都是中断线程,但是后者更安全、有条理和高效,其原因跟推荐使用 而不直接使用Thread类是一致的。所以结合上面讲到的原则,我们应尽量采用cancel()方法,调用线程管理器ExecutorService接口的 ( task) 方法会返回一个Future<?>对象,然后调用Future.cancel()的方法来取消任务,并返回一个boolean值。