Java线程中断的本质深⼊理解(转)
⼀、Java中断的现象
⾸先,看看Thread类⾥的⼏个⽅法:
public static boolean interrupted 测试当前线程是否已经中断。线程的中断状态由该⽅法清除。换句话说,如果连续两次调⽤该⽅法,则第⼆次调⽤将返回 fal(在第⼀次调⽤已清除了其中断状态之后,且第⼆次调⽤检验完中断状态前,当前线程再次中断的情况除外)。
public
boolean isInterrupted()测试线程是否已经中断。线程的中断状态不受该⽅法的影响。
public void interrupt()中断线程。
上⾯列出了与中断有关的⼏个⽅法及其⾏为,可以看到interrupt是中断线程。如果不了解Java的中断机制,这样的⼀种解释极容易造成误解,认为调⽤了线程的interrupt⽅法就⼀定会中断线程。
其实,Java的中断是⼀种协作机制。也就是说调⽤线程对象的interrupt⽅法并不⼀定就中断了正在运⾏
的线程,它只是要求线程⾃⼰在合适的时机中断⾃⼰。每个线程都有⼀个boolean的中断状态(不⼀定就是对象的属性,事实上,该状态也确实不是Thread的字段),interrupt ⽅法仅仅只是将该状态置为true
复制代码代码如下:
public class TestInterrupt {
public static void main(String[] args) {
Thread t = new MyThread();
t.start();
t.interrupt();
System.out.println("已调⽤线程的interrupt⽅法");
}
static class MyThread extends Thread {
public void run() {
int num = longTimeRunningNonInterruptMethod(2, 0);
System.out.println("长时间任务运⾏结束,num=" + num);
System.out.println("线程的中断状态:" + Thread.interrupted());
}
private static int longTimeRunningNonInterruptMethod(int count, int initNum) {
for(int i=0; i<count; i++) {
for(int j=0; j<Integer.MAX_VALUE; j++) {
initNum ++;
}
}
return initNum;
}
}
}
⼀般情况下,会打印如下内容:
已调⽤线程的interrupt⽅法
长时间任务运⾏结束,num=-2
线程的中断状态:true
可见,interrupt⽅法并不⼀定能中断线程。但是,如果改成下⾯的程序,情况会怎样呢?
复制代码代码如下:
import urrent.TimeUnit;
public class TestInterrupt {
public static void main(String[] args) {
Thread t = new MyThread();
t.start();
t.interrupt();
System.out.println("已调⽤线程的interrupt⽅法");
}
static class MyThread extends Thread {
public void run() {
int num = -1;
try {
num = longTimeRunningInterruptMethod(2, 0);
} catch (InterruptedException e) {
System.out.println("线程被中断");
throw new RuntimeException(e);
}
System.out.println("长时间任务运⾏结束,num=" + num);
System.out.println("线程的中断状态:" + Thread.interrupted());
}
private static int longTimeRunningInterruptMethod(int count, int initNum) throws InterruptedException{
for(int i=0; i<count; i++) {
TimeUnit.SECONDS.sleep(5);
}
return initNum;
}
}
}
经运⾏可以发现,程序抛出异常停⽌了,run⽅法⾥的后两条打印语句没有执⾏。那么,区别在哪⾥?
⼀般说来,如果⼀个⽅法声明抛出InterruptedException,表⽰该⽅法是可中断的(没有在⽅法中处理中断却也声明抛出InterruptedException的除外),也就是说可中断⽅法会对interrupt调⽤做出响应(例如sleep响应interrupt的操作包括清除中断状态,抛出InterruptedException),如果interrupt调⽤是在可中断⽅法之前调⽤,可中断⽅法⼀定会处理中断,像上⾯的例⼦,interrupt⽅法极可能在run未进⼊sleep的时候就调⽤了,但sleep检测到中断,就会处理该中断。如果在可中断⽅法正在执⾏中的时候调⽤interrupt,会怎么样呢?这就要看可中断⽅法处理中断的时机了,只要可中断⽅法能检测到中断状态为true,就应该处理中断。让我们为开头的那段代码加上中断处理。
那么⾃定义的可中断⽅法该如何处理中断呢?那就是在适合处理中断的地⽅检测线程中断状态并处理。
复制代码代码如下:
public class TestInterrupt {
public static void main(String[] args) throws Exception {
Thread t = new MyThread();
t.start();
// TimeUnit.SECONDS.sleep(1);//如果不能看到处理过程中被中断的情形,可以启⽤这句再看看效果
t.interrupt();
System.out.println("已调⽤线程的interrupt⽅法");
}
static class MyThread extends Thread {
public void run() {
int num;
try {
num = longTimeRunningNonInterruptMethod(2, 0);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("长时间任务运⾏结束,num=" + num);
System.out.println("线程的中断状态:" + Thread.interrupted());
}
private static int longTimeRunningNonInterruptMethod(int count, int initNum) throws InterruptedException {
if(interrupted()) {
throw new InterruptedException("正式处理前线程已经被请求中断");
}
for(int i=0; i<count; i++) {
for(int j=0; j<Integer.MAX_VALUE; j++) {
initNum ++;
}
//假如这就是⼀个合适的地⽅
if(interrupted()) {
//回滚数据,清理操作等
throw new InterruptedException("线程正在处理过程中被中断");
}
}
return initNum;
}
}
}
如上⾯的代码,⽅法longTimeRunningMethod此时已是⼀个可中断的⽅法了。在进⼊⽅法的时候判断是否被请求中断,如果是,就不进⾏相应的处理了;处理过程中,可能也有合适的地⽅处理中断,例如上⾯最内层循环结束后。
这段代码中检测中断⽤了Thread的静态⽅法interrupted,它将中断状态置为fal,并将之前的状态返回,⽽isInterrupted只是检测中断,并不改变中断状态。⼀般来说,处理过了中断请求,应该将其状态置为fal。但具体还要看实际情形。
⼆、Java中断的本质
在历史上,Java试图提供过抢占式限制中断,但问题多多,例如已被废弃的Thread.stop、Thread.suspend和 sume等。另⼀⽅⾯,出于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的实现和硬件的性能。
三、⼀些不会抛出 InterruptedException 的线程阻塞操作
然⽽,对于某些线程阻塞操作,JVM并不会⾃动抛出InterruptedException异常。例如,某些I/O操作和内部锁操作。对于这类操作,可以⽤其他⽅式模拟中断:
1)java.io中的异步socket I/O
读写socket的时候,InputStream和OutputStream的read和write⽅法会阻塞等待,但不会响应java中断。不过,调⽤Socket的clo⽅法后,被阻塞线程会抛出SocketException异常。
2)利⽤Selector实现的异步I/O
如果线程被阻塞于Selector.lect(在java.nio.channels中),调⽤wakeup⽅法会引起ClodSelectorException异常。
3)锁获取
如果线程在等待获取⼀个内部锁,我们将⽆法中断它。但是,利⽤Lock类的lockInterruptibly⽅法,我们可以在等待锁的同时,提供中断能⼒。
四、两条编程原则
另外,在任务与线程分离的框架中,任务通常并不知道⾃⾝会被哪个线程调⽤,也就不知道调⽤线程处理中断的策略。所以,在任务设置了线程中断标记后,并不能确保任务会被取消。因此,有以下两条编程原则:
1)除⾮你知道线程的中断策略,否则不应该中断它。
这条原则告诉我们,不应该直接调⽤Executer之类框架中线程的interrupt⽅法,应该利⽤诸如Future.cancel的⽅法来取消任务。
2)任务代码不该猜测中断对执⾏线程的含义。
这条原则告诉我们,⼀般代码遇在到InterruptedException异常时,不应该将其捕获后“吞掉”,⽽应该继续向上层代码抛出。
总之,Java中的⾮抢占式中断机制,要求我们必须改变传统的抢占式中断思路,在理解其本质的基础上,采⽤相应的原则和模式来编程。