Contents

Java 多线程 - 线程中断 Interrupt

线程interrupt,是一个非常重要的API,也是经常使用的方法,在本文中我们将Thread深入源码对其进行详细的剖析。

首先来看一下与线程中断相关的几个API:

public void interrupt()
public static boolean interrupted()
public boolean isInterrupted()    

interrupt

如下方法的调用会使得当前线程进入阻塞状态,而调用当前线程的interrupt方法,就可以打断阻塞。

  • Object的wait方法。
  • Object的wait(long)方法。
  • Object的wait(long,int)方法。
  • Thread的sleep(long)方法。
  • Thread的sleep(long,int)方法。
  • Thread的join方法。
  • Thread的join(long)方法。
  • Thread的join(long,int)方法。
  • InterruptibleChannel的io操作。
  • Selector的wakeup方法。

上述若干方法都会使得当前线程进入阻塞状态,若另外的一个线程调用被阻塞线程的interrupt方法,则会打断这种阻塞,因此这种方法有时会被称为可中断方法,记住,打断一个线程并不等于该线程的生命周期结束,仅仅是打断了当前线程的阻塞状态。

一旦线程在阻塞的情况下被打断,都会抛出一个称为InterruptedException的异常,这个异常就像一个signal(信号)一样通知当前线程被打断了,下面我们来看一个例子:

import java.util.concurrent.TimeUnit;
public class ThreadInterrupt {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            try {
                TimeUnit.MINUTES.sleep(1);
            } catch(
                InterruptedException e)

            {
                System.out.println("Oh, i am be interrupted.");
            }
        });
        thread.start();
        //short block and make sure thread is started.
        TimeUnit.MILLISECONDS.sleep(2);
        thread.interrupt();
    }
}

上面的代码创建了一个线程,并且企图休眠1分钟的时长,不过很可惜,大约在2毫秒之后就被主线程调用interrupt方法打断,程序的执行结果就是“Oh,i am be interrupted.”

interrupt这个方法到底做了什么样的事情呢?在一个线程内部存在着名为interrupt flag的标识,如果一个线程被interrupt,那么它的flag将被设置,但是如果当前线程正在执行可中断方法被阻塞时,调用interrupt方法将其中断,反而会导致flag被清除,关于这点我们在后面还会做详细的介绍。另外有一点需要注意的是,如果一个线程已经是死亡状态,那么尝试对其的interrupt会直接被忽略。

isInterrupted

isInterrupted是Thread的一个成员方法,它主要判断当前线程是否被中断,该方法仅仅是对interrupt标识的一个判断,并不会影响标识发生任何改变,这个与我们即将学习到的interrupted是存在差别的,下面我们看一个简单的程序:

public class ThreadisInterrupted {
	public static void main(String[] args) throws InterruptedException {
		Thread thread = new Thread() {
			@Override
			public void run() {
				while (true) {
					//do nothing, just empty loop.
				}
			}
		};
		thread.start();
		TimeUnit.MILLISECONDS.sleep(2);
		System.out.printf("Thread is interrupted ? %s\n", thread.isInterrupted());
		thread.interrupt();
		System.out.printf("Thread is interrupted ? %s\n", thread.isInterrupted());
	}
}

上面的代码中定义了一个线程,并且在线程的执行单元中(run方法)写了一个空的死循环,为什么不写sleep呢?因为sleep是可中断方法,会捕获到中断信号,从而干扰我们程序的结果。下面是程序运行的结果,记得手动结束上面的程序运行,或者你也可以将上面定义的线程指定为守护线程,这样就会随着主线程的结束导致JVM中没有非守护线程而自动退出。

Thread is interrupted ? false
Thread is interrupted ? true

可中断方法捕获到了中断信号(signal)之后,也就是捕获了InterruptedException异常之后会擦除掉interrupt的标识,对上面的程序稍作修改,你会发现程序的结果又会出现很大的不同,示例代码如下:

public class ThreadisInterrupted {
	public static void main(String[] args) throws InterruptedException {
		Thread thread = new Thread() {
			@Override
			public void run() {
				while (true) {
					try {
						TimeUnit.MINUTES.sleep(1);
					} catch (InterruptedException e) {
						//ignore the exception
						//here the interrupt flag will be clear.
						//由于可中断方法的异常被捕获后,会擦除掉interrup标记,所以调用
						//isInterrupted返回false
						System.out.printf("I am be interrupted ? %s", isInterrupted());
					}
				}
			}
		};
		thread.start();
		TimeUnit.MILLISECONDS.sleep(2);
		System.out.printf("Thread is interrupted ? %s\n", thread.isInterrupted());
		thread.interrupt();
		System.out.printf("Thread is interrupted ? %s\n", thread.isInterrupted());
	}
}

由于在run方法中使用了sleep这个可中断方法,它会捕获到中断信号,并且会擦除interrupt标识,因此程序的执行结果都会是false,程序输出如下:

Thread is interrupted ? false
I am be interrupted ? false
Thread is interrupted ? false

其实这也不难理解,可中断方法捕获到了中断信号之后,为了不影响线程中其他方法的执行,将线程的interrupt标识复位是一种很合理的设计。

interrupted

interrupted是一个静态方法,虽然其也用于判断当前线程是否被中断,但是它和成员方法isInterrupted还是有很大的区别的,调用该方法会直接擦除掉线程的interrupt标识,需要注意的是,如果当前线程被打断了,那么第一次调用interrupted方法会返回true,并且立即擦除了interrupt标识;第二次包括以后的调用永远都会返回false,除非在此期间线程又一次地被打断,下面设计了一个简单的例子,来验证我们的说法:

public class ThreadisInterrupted {
	public static void main(String[] args) throws InterruptedException {
		Thread thread = new Thread() {
			@Override
			public void run() {
				while (true) {
					System.out.println(Thread.interrupted());
				}
			}
		};
		thread.setDaemon(true);
		thread.start();
		//shortly block make sure the thread is started.
		TimeUnit.MILLISECONDS.sleep(2);
		thread.interrupt();
	}
}

同样由于不想要受到可中断方法如sleep的影响,在Thread的run方法中没有进行任何短暂的休眠,所以运行上面的程序会出现非常多的输出,但是我们通过对输出的检查会发现如下所示的内容,其足以作为对该方法的解释。

……
false
false
true
false
false
……

在很多的false包围中发现了一个true,也就是interrupted方法判断到了其被中断,立即擦除了中断标识,并且只有这一次返回true,后面的都将会是false。

interrupt 注意事项

打开Thread的源码,不难发现,isInterrupted方法和interrupted方法都调用了同一个本地方法:

private native boolean isInterrupted(boolean ClearInterrupted);

其中参数ClearInterrupted主要用来控制是否擦除线程interrupt的标识。isInterrupted方法的源码中该参数为false,表示不想擦除:

public boolean isInterrupted() {
    return isInterrupted(false);
}

而interrupted静态方法中该参数则为true,表示想要擦除:

public static boolean interrupted() {
    return currentThread().isInterrupted(true);
}

在比较详细地学习了interrupt方法之后,大家思考一个问题,如果一个线程在没有执行可中断方法之前就被打断,那么其接下来将执行可中断方法,比如sleep会发生什么样的情况呢?下面我们通过一个简单的实验来回答这个疑问:

public class ThreadisInterrupted {
	public static void main(String[] args) {
		//① 判断当前线程是否被中断
        // flag=false,清除线程中断标志
		System.out.println("Main thread is interrupted? " + Thread.interrupted());
		//②中断当前线程
        // flag=true
		Thread.currentThread().interrupt();
		//③判断当前线程是否已经被中断
        // 这里不能再调用Thread.interrupted(),因为会将flag清除,达不到实验效果
		System.out.println("Main thread is interrupted? " + Thread.currentThread().isInterrupted());
		try {
			//④ 当前线程执行可中断方法
			TimeUnit.MINUTES.sleep(1);
		} catch (InterruptedException e) {
			//⑤捕获中断信号
			System.out.println("I will be interrupted still.");
		}
	}
}

通过运行上面的程序,你会发现,如果一个线程设置了interrupt标识,那么接下来的可中断方法会立即中断,因此注释⑤的信号捕获部分代码会被执行.

参考

【1】《Java 高并发编程详解》-汪文君