11.3.2 线程中断
线程中断是线程之间的一种通信方式。通过一个线程对应的Thread类的对象的interrupt方法可以向该线程发出中断请求。根据线程当前所处的状态不同,中断一个线程会产生不同的效果。在一般的情况下,中断一个线程会在线程对应的Thread类的对象上设置一个标记。该标记用来记录当前的中断状态。通过Thread类的isInterrupted方法可以查询此标记来判断是否有中断请求发生。当线程A通过调用线程B的interrupt方法来发出中断请求时,线程A再向线程B发出一个信号。线程B应该在方便的时候来处理这个中断请求,当然这不是必须的。一个线程可以选择忽略所有或部分中断请求。
Object类的wait方法及Thread类的join方法和sleep方法都会抛出受检异常java.lang.InterruptedException。在调用这3个方法及其重载形式时,必须捕获InterruptedException异常并进行处理。当线程由于调用这3个方法而进入等待状态时,通过interrupt方法中断该线程会导致该线程离开等待状态。对于wait方法调用来说,线程需要在重新获取到监视器对象上的锁之后才能抛出InterruptedException异常,并执行对InterruptedException异常的处理逻辑。
线程中断的一个典型应用场景是实现可取消的任务。有些线程在执行任务时会使用一个无限循环来重复执行。这时需要一种方式来结束线程的运行。一种做法是使用之前介绍的volatile变量作为结束标记;另外一种做法是向线程发出中断请求。仍然以生产者-消费者场景为例,对于生产者线程来说,只要缓冲区不为空,就会不断运行,产生新的数据。可以在循环条件中加上对当前线程对应的Thread类的对象的isInterrupted方法的调用,用来检查是否收到了中断请求。如果收到了中断请求,可以终止执行。
对InterruptedException异常需要谨慎地进行处理。在大多数时候,开发人员只是为了能够通过编译,简单地捕获InterruptedException异常,而不进行任何处理。这种做法通常是不合适的。当线程由于调用wait、join和sleep方法进入等待状态时,如果收到中断请求,线程就会进入InterruptedException异常的处理逻辑。如果在当前层次上处理InterruptedException异常是合理的,就直接进行处理;否则应该把InterruptedException异常重新抛出,由调用者进行处理。在InterruptedException异常发生时,当前线程对应的Thread类的对象内部的中断标记会被清空,相当于该中断请求已经被InterruptedException异常的处理逻辑处理了。如果在当前InterruptedException异常的处理代码中不适合处理该异常,又无法把该异常重新抛出,则需要通过interrupt方法来重新中断该线程。这样就保存了当前线程曾经被中断过的状态信息,可以让后续代码来处理该中断请求。
Thread类中还有一个与线程中断相关的方法interrupted。该方法不但可以判断当前线程是否被中断,还可以清除线程内部的中断标记。如果调用interrupted方法的返回值为true,说明该线程曾经被中断过。在interrupted方法调用完成之后,线程内部的中断标记已经被清空。
如果一个线程当前处于某个对象所关联的等待集合中,那么中断该线程或发出唤醒通知都可以使该线程从等待集合中被移除。如果两者同时发生,那么具体的运行结果取决于两者的实际发生顺序,由虚拟机实现来确定。通过notify方法发出的唤醒请求不会被线程中断所影响。当调用某个对象上的notify方法时,至少有一个线程被唤醒,或者所有的线程都被中断,唤醒请求不会丢失。如果一个线程被选为唤醒的对象,而该线程同时又被中断,并且虚拟机选择让该线程中断,那么等待集合中的另外一个线程必须被唤醒。