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标识,那么接下来的可中断方法会立即中断,因此注释⑤的信号捕获部分代码会被执行.