Java
Java多线程

Java多线程 07 - 线程的中断

简介:Java线程之中,一个线程的生命周期分为:初始、就绪、运行、阻塞以及结束。一般而言,可能有三种原因引起阻塞:等待阻塞、同步阻塞以及其他阻塞(睡眠、IO阻塞等);等待阻塞是调用wait()方法产生的,同步阻塞则是由同步块synchronized同步锁产生的,睡眠阻塞是由sleep()产生的。

1. interrupt()方法简介

Java线程之中,一个线程的生命周期分为:初始、就绪、运行、阻塞以及结束。一般而言,可能有三种原因引起阻塞:等待阻塞、同步阻塞以及其他阻塞(睡眠、IO阻塞等);等待阻塞是调用wait()方法产生的,同步阻塞则是由同步块synchronized同步锁产生的,睡眠阻塞是由sleep()产生的。

要中断一个Java线程,可调用线程类(Thread)对象的实例方法interrupte();然而interrupte()方法并不会立即执行中断操作;具体而言,这个方法只会给线程设置一个为true的中断标志(中断标志只是一个布尔类型的变量),而设置之后,则根据线程当前的状态进行不同的后续操作。

  1. 如果线程的当前状态处于阻塞状态,那么在将中断标志设置为true后,还会有如下三种情况之一的操作:
  • 如果是wait()sleep()以及join()三个方法引起的阻塞,那么它的中断状态会被清除并且会收到一个InterruptedException异常。例如,线程通过wait()进入阻塞状态,此时通过interrupt()中断该线程;调用interrupt()会立即将线程的中断标记设为true,但是由于线程处于阻塞状态,所以该中断标记会立即被清除为false,同时,会产生一个InterruptedException的异常。
  • 如果是java.nio.channels.InterruptibleChannel进行的IO操作引起的阻塞,则会对线程抛出一个ClosedByInterruptedException
  • 如果是轮询(java.nio.channels.Selectors)引起的线程阻塞,则立即返回,不会抛出异常。
  1. 如果线程的当前状态处于非阻塞状态,那么仅仅是线程的中断标志被修改为true。但在此基础上,一旦进入阻塞状态,则按照阻塞状态的情况来进行处理;例如,一个线程在运行状态中,其中断标志被设置为true,则此后一旦线程调用了wait()sleep()以及jion()三个方法中的一种,立马抛出一个InterruptedException,且中断标志被清除,重新设置为false

通过上面的分析,我们可以总结,调用线程类的interrupt()方法,其本质只是设置该线程的中断标志,将中断标志设置为true,并根据线程状态决定是否抛出异常。因此,通过interrupt()方法真正实现线程的中断原理是:开发人员根据中断标志的具体值,来决定如何退出线程。interrupt()方法源码如下:

  • public void interrupt() {
  • if (this != Thread.currentThread())
  • checkAccess();
  • synchronized (blockerLock) {
  • Interruptible b = blocker;
  • if (b != null) {
  • interrupt0(); // Just to set the interrupt flag
  • b.interrupt(this);
  • return;
  • }
  • }
  • interrupt0();
  • }
  • private native void interrupt0();
  • public boolean isInterrupted() {
  • return isInterrupted(false);
  • }
  • public static boolean interrupted() {
  • return currentThread().isInterrupted(true);
  • }
  • private native boolean isInterrupted(boolean ClearInterrupted);
  • interrupt()内部调用了interrupt0()方法,它的作用是Just to set the interrupt flag,即仅仅是设置中断标识位,interrupt0()被native修饰的,由Java虚拟机实现的。
  • isInterrupted()方法唯一的作用只是测试线程是否已经中断,中断标识位的状态并不受到该方法的影响。isInterrupted()最终调用的是isInterrupted(boolean ClearInterrupted),这个也是一个native方法,由Java虚拟机实现,参数ClearInterrupted决定是否清除中断标识位,isInterrupted()中传递的是false,即不清除。
  • interrupted()isInterrupted()方法一样,唯一的区别在于interrupted()的内部调用的是currentThread().isInterrupted(true),所以interrupted()会清除线程的中断标识位。

2. interrupt()最佳实践

通常使用interrupt()的最佳实践是,可以使用interrupt()方法来修改isInterrupted()返回的结果,根据isInterrupted()返回的结果来决定某一部分任务的执行。如对于一个线程我们分为阻塞和非阻塞两种状态,针对该线程内某一部分任务,中断有以下的处理:

  1. 非阻塞的状态

在非阻塞状态下可以通过isInterrupted()决定某一段任务是否执行:

  • class InterruptThread extends Thread {
  • @Override
  • public void run() {
  • // 循环执行,如果 isInterrupted() 为true则停止执行
  • for (int i = 0; !isInterrupted() && i < Integer.MAX_VALUE; i++) {
  • System.out.println(Thread.currentThread().getName() + " (" + this.getState() + ") executed " + i);
  • }
  • System.out.println(Thread.currentThread().getName() + " (" + this.getState() + ") continued.");
  • }
  • }

如果该线程正在运行,在对该线程调用了interrupt()操作(可能是线程自己调用,也可能是其他并行执行的线程调用),将会将其标志位改为true,因此for循环将终止执行,进而执行剩下的任务。

  1. 阻塞状态

在上面的第一种情况中,虽然对非阻塞状态可控,但当该线程处于阻塞状态时,对其调用interrupt()操作会抛出InterruptedException异常,会使线程直接终止,因此我们需要对InterruptedException异常进行捕获:

  • class InterruptThread extends Thread {
  • @Override
  • public void run() {
  • try {
  • // 循环执行,如果 isInterrupted() 为true则停止执行
  • for (int i = 0; i < Integer.MAX_VALUE; i++) {
  • System.out.println(Thread.currentThread().getName() + " (" + this.getState() + ") executed " + i);
  • }
  • } catch (Exception e) {
  • System.out.println(Thread.currentThread().getName() + " (" + this.getState() + ") catch InterruptedException.");
  • }
  • System.out.println(Thread.currentThread().getName() + " (" + this.getState() + ") continued.");
  • }
  • }

在上述代码中,对该线程中可能会被中断的循环操作进行了try…catch处理,如果该线程处于阻塞状态,对其调用interrupt()操作抛出的InterruptedException异常会被捕获,同时for循环将被终止执行,进而执行剩下的任务。

3. interrupt()案例

我们根据上述代码,分别编写阻塞和非阻塞两种情况下的测试案例。

3.1. 阻塞情况的interrupt操作

首先是阻塞情况下的interrupt操作,示例代码如下:

代码如下:

  • class InterruptThread extends Thread {
  • @Override
  • public void run() {
  • try {
  • for (int i = 0; i < Integer.MAX_VALUE; i++) {
  • Thread.sleep(300);
  • System.out.println(Thread.currentThread().getName() + " (" + this.getState() + ") executed " + i);
  • }
  • } catch (Exception e) {
  • System.out.println(Thread.currentThread().getName() + " (" + this.getState() + ") catch InterruptedException.");
  • }
  • System.out.println(Thread.currentThread().getName() + " (" + this.getState() + ") continued.");
  • }
  • }
  • public class InterruptTest {
  • public static void main(String[] args) {
  • // 启动测试线程
  • Thread thread = new InterruptThread();
  • thread.start();
  • // 主线程等待1秒
  • try {
  • Thread.sleep(1000);
  • } catch (InterruptedException e) {
  • e.printStackTrace();
  • }
  • // 主线程开始执行对thread线程的中断
  • System.out.println(Thread.currentThread().getName() + " (" + Thread.currentThread().getState() + ") execute thread.interrupt().");
  • thread.interrupt();
  • // 查看thread线程状态
  • System.out.println(thread.getName() + " (" + thread.getState() + ") is interrupted.");
  • // 主线程等待1秒,以获取准确的thread线程状态
  • try {
  • Thread.sleep(1000);
  • } catch (InterruptedException e) {
  • e.printStackTrace();
  • }
  • System.out.println(thread.getName() + " (" + thread.getState() + ") is interrupted now.");
  • }
  • }

某一次运行的结果如下:

  • Thread-0 (RUNNABLE) executed 0
  • Thread-0 (RUNNABLE) executed 1
  • Thread-0 (RUNNABLE) executed 2
  • main (RUNNABLE) execute thread.interrupt().
  • Thread-0 (TIMED_WAITING) is interrupted.
  • Thread-0 (RUNNABLE) catch InterruptedException.
  • Thread-0 (RUNNABLE) continued.
  • Thread-0 (TERMINATED) is interrupted now.

运行过程详解:

  1. 在main方法中,主线程开始执行,并且创建并启动了thread-0线程,然后主线程开始休眠1秒。
  2. 此时thread-0线程一直在执行并且打印run()方法循环体内的信息,由于循环体内存在休眠操作,因此thread-0线程会有大部分时间处于阻塞状态。
  3. 在主线程对thread-0线程执行了interrupt()操作,此时thread-0线程有很大几率正处于阻塞状态,因此会抛出一个InterruptedException异常。因此会跳出循环体。
  4. thread-0线程继续执行剩余的代码。

3.2. 非阻塞情况的interrupt操作

在上述代码中,如果去掉第7行InterruptThread类中的Thread.sleep(300);操作,在主线程对thread-0线程执行了interrupt()操作时,thread-0线程可能正处于非阻塞状态,此时是不会抛出InterruptedException异常的,因此不会终止for循环。如果要在interrupt()操作之后终止for循环,还需要在for循环内设置!isInterrupted()false时for循环终止的条件。因为非阻塞状态下,中断操作会直接将thread-0线程的中断标志位置为true,此时就能保证for循环操作在非阻塞状态下都能够响应interrupt()操作从而结束循环。测试代码如下:

  • class InterruptThread extends Thread {
  • @Override
  • public void run() {
  • // 循环执行,如果 isInterrupted() 为true则停止执行
  • for (int i = 0; !isInterrupted() && i < Integer.MAX_VALUE; i++) {
  • System.out.println(Thread.currentThread().getName() + " (" + this.getState() + ") executed " + i);
  • }
  • System.out.println(Thread.currentThread().getName() + " (" + this.getState() + ") continued.");
  • }
  • }
  • public class InterruptTest {
  • public static void main(String[] args) {
  • // 启动测试线程
  • Thread thread = new InterruptThread();
  • thread.start();
  • // 主线程等待1秒
  • try {
  • Thread.sleep(1000);
  • } catch (InterruptedException e) {
  • e.printStackTrace();
  • }
  • // 主线程开始执行对thread线程的中断
  • System.out.println(Thread.currentThread().getName() + " (" + Thread.currentThread().getState() + ") execute thread.interrupt().");
  • thread.interrupt();
  • // 查看thread线程状态
  • System.out.println(thread.getName() + " (" + thread.getState() + ") is interrupted.");
  • // 主线程等待1秒,以获取准确的thread线程状态
  • try {
  • Thread.sleep(1000);
  • } catch (InterruptedException e) {
  • e.printStackTrace();
  • }
  • System.out.println(thread.getName() + " (" + thread.getState() + ") is interrupted now.");
  • }
  • }

运行结果如下:

  • ...
  • Thread-0 (RUNNABLE) executed 178952
  • Thread-0 (RUNNABLE) executed 178953
  • Thread-0 (RUNNABLE) executed 178954
  • main (RUNNABLE) execute thread.interrupt().
  • Thread-0 (RUNNABLE) is interrupted.
  • Thread-0 (RUNNABLE) continued.
  • Thread-0 (TERMINATED) is interrupted now.

可以发现,相对于前面阻塞的情况,Thread-0 (RUNNABLE) is interrupted.显示出在对thread-0线程执行interrupt操作时,thread-0线程正处于RUNNABLE状态,但由于for循环对isInterrupted()进行了判断,也会在thread-0线程调用interrupt操作时终止循环。

3.3. 综合处理的最佳实践

如果想要同时处理阻塞和非阻塞两种情况下的中断操作,我们就应该将上面两种情况综合起来,这也是使用interrupt的最佳实践,代码如下:

  • class InterruptThread extends Thread {
  • @Override
  • public void run() {
  • try {
  • // 循环执行,如果 isInterrupted() 为true则停止执行
  • for (int i = 0; !isInterrupted() && i < Integer.MAX_VALUE; i++) {
  • Thread.sleep(300);
  • System.out.println(Thread.currentThread().getName() + " (" + this.getState() + ") executed " + i);
  • }
  • } catch (Exception e) {
  • System.out.println(Thread.currentThread().getName() + " (" + this.getState() + ") catch InterruptedException.");
  • }
  • System.out.println(Thread.currentThread().getName() + " (" + this.getState() + ") continued.");
  • }
  • }
  • public class InterruptTest {
  • public static void main(String[] args) {
  • // 启动测试线程
  • Thread thread = new InterruptThread();
  • thread.start();
  • // 主线程等待1秒
  • try {
  • Thread.sleep(1000);
  • } catch (InterruptedException e) {
  • e.printStackTrace();
  • }
  • // 主线程开始执行对thread线程的中断
  • System.out.println(Thread.currentThread().getName() + " (" + Thread.currentThread().getState() + ") execute thread.interrupt().");
  • thread.interrupt();
  • // 查看thread线程状态
  • System.out.println(thread.getName() + " (" + thread.getState() + ") is interrupted.");
  • // 主线程等待1秒,以获取准确的thread线程状态
  • try {
  • Thread.sleep(1000);
  • } catch (InterruptedException e) {
  • e.printStackTrace();
  • }
  • System.out.println(thread.getName() + " (" + thread.getState() + ") is interrupted now.");
  • }
  • }

上面的代码既对InterruptedException异常进行了捕获,也使用isInterrupted()进行了循环体是否继续的判断,可以保证在阻塞和非阻塞两种情况下都响应interrupt事件。