被忽略的InterruptedException

我们在写Java程序的时候,经常会调用Thread.sleep()来让线程休眠一会。而后我需要用try catch将其包裹起来,如果不这样做程序不会通过编译,因为sleep方法会抛出受检查类型的异常(checked exception)InterruptedException。但是很多的人做法是捕捉异常后不会做任何处理,或者说只会打印异常堆栈信息。然而这样做是否合适,sleep方法又为何会抛出InterruptedException

1
2
3
4
5
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}

如何停止一个线程 ?

首先我们先探讨一下,在Java中如何让一个Thread停止运行。

stop 方法

Java 曾为Thread提供过stop方法,用来让一个Thread停止运行,不过已经被标记为Deprecated,理由是stop是个很危险的方法。

1
2
3
4
5
6
7
8
class Task extends Thread{
@Override
public void run() {
acquire();
doSomeThing...
release();
}
}

上面的代码Task为完成doSomeThing的任务,会申请一些资源,完成后会将这些资源释放。假设我们为Task创建一个实列,然后我我们调用start方法,使线程运行,之后在某个时间段我们调用stop方法让线程终止执行。假如这时候线程线程正在执行doSomething的某条指令,这时候线程突然终止,就会带来一个问题前面申请的资源没有释放,线程就结束了。

那我们有没有办法监听到线程被 stop,然后释放资源呢 ?

ThreadDeath

stop方法停止线程的方式是抛出一个ThreadDeath错误,这样我们可以通过捕捉ThreadDeath来释放资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Task extends Thread {
@Override
public void run() {
try {
acquire();
doSomeThing...
} catch (ThreadDeath e) {
...
} finally {
release();
}
}
}

看起来我们的问题解决了?

一个重要的问题是我们的并不知道线程究竟是在哪一条指令停止了运行,我的善后工作可能也只是放弃本次线程做执行的任务,并恢复至线程执行之前的状态。

stop 方法对监视器的影响

现在假设有一个多个线程协作的场景,Task1的执行依赖于Task0Task0Task1持有同一个monitor。我们依次调用Task0Task1start()方法让它们执行,Task0执行过程中,Task1处于阻塞状态,直到Task0执行完毕释放了锁Task1才开始执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Task0 extends Thread{
@Override
public void run() {
synchronized(lock){
doSomeThing...
release();
}
}
}

class Task1 extends Thread{
@Override
public void run() {
synchronized(lock){
doSomeThingElse...
}
}
}

通过stop()方法停止一个线程,会释放其所有的监视器如果在Task0执行过程中,我们调用了其stop方法,让其停止运行。这时Task0并没有完成其工作,然后释放了锁。之后Task1就获取的执行权,我们上面提到Task1的执行要依赖于Task0,然而 Task0并未执行完成。Task1所执行时使用的时Task0产生的错误的不完整的数据。

这时候我即使捕捉了ThreadDeath错误也很难解决问题,锁总归是要被释放的,我们不能保证其他竞争该monitor的其他线程安全的执行,然而,对此我几乎无能为力。

所以stop方法停止一个线程是一种粗暴的方式,可能会给我们的程序带来严重的后果。就像你正在用电脑工作,这时候有人突然把电源给你拔掉了。我们如何优雅安全的终止一个线程呢 ?

interrupt 方法

前面讲到使用stop方法粗暴的停止一个线程是很不安全的,已经被标记为Deprecated,替代的方法就是interrupt。每一个线程都会维护一个中断状态,通过调用线程interrupt方法的方式将线程标记为中断状态,让线程通过中断状态自己终止执行,线程何时停止运行、在何处停止运行、甚至是否停止运行都有线程本身自己决定。

interrupt 相关的方法

1
2
3
void interrupt()  //将一个线程标记为中断 状态
boolean isInterrupted(); // 判断线程是否处于中断状态
static boolean interrupted(); // 判断线程是否处于中断状态,并清除线程的中断状态
  • interrupt()将一个线程标记为中断状态
  • isInterrupted()判断线程是否处于中断状态
  • interrupted()是一个静态方法,会重置当前线程的中断状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Task extends Thread {
@Override
public void run() {
while (!isInterrupted()) {
doSomeThing...
}
}
}

public static void main(String args[]) {
Interrupt.Task task = new Interrupt.Task();
task.start();
...
task.interrupt();
}

上面代码Task要执行的是一个周期任务,我们可以将每一次任务的间隔作为安全点,判断线程是否被中断。Task在执行的过程,在其他线程被调用interrupt方法,线程Task通过 isInterrupted()检测是否被中断,若是中断状态后停止while循环退出run方法。
上面的例子在执行周期任务,在执行短暂的非周期任务时,可能我并不需要关注中断状态

interrupt 与阻塞方法

阻塞方法何时执行完成我们一般不可控,例如wait方法何时执行完成依赖于有没有被其他线程唤醒。为了以保证线程响应中断指令,如果被调用了interrupt方法的线程处于休眠阻塞状态,该线程会被立刻切换至可执行状态,不会等待其阻塞方法执行完成。实现方式就是阻塞方法抛出 InterruptedException,一个阻塞方法抛出一个InterruptedException,代表阻塞休眠任务并未完成,当前线程被中断。 (IO相关操作不允许被强制中断,所以与IO相关的阻塞方法不会抛出InterruptedException)

1
2
3
4
5
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
...
}

InterruptedException 抛出后会清除中断标记

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Task extends Thread {
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) { //抛出InterruptedException后线程的中断状态被清除
}

while (!isInterrupted()) {
doSomeThing...
}
}
}

public static void main(String args[]) {
Interrupt.Task task = new Interrupt.Task();
task.start();
...
task.interrupt();
}

上面的代码线程 Task 启动后,在Thread.sleep(5000)的执行过程中我们调用了interrupt方法,但是由于我们捕捉了 sleep方法的InterruptedException异常,于是线程的中断状态又被清除了。此时while (!isInterrupted())将永远是 true。所以为了保证后面代码能够正确响应线程的中断状态,我们在捕捉完InterruptedException异常做完处理之后要手动的将线程设为中断状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Task extends Thread {
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
//....
Thread.currentThread().interrupt();
}

while (!isInterrupted()) {
doSomeThing...
}
}
}

public static void main(String args[]) {
Interrupt.Task task = new Interrupt.Task();
task.start();
...
task.interrupt();
}

遇到 InterruptedException 的处理方式

  • method0 处理完处理完InterruptedException,重新将线程设为中断状态
  • method1 直接抛出
  • method2 处理完InterruptedException,重新将其抛出

总之不要生吞 InterruptedException,要让后续代码能后正确的响应线程的中断状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class Task extends Thread {
@Override
public void run() {
method0();
try {
method1();
} catch (InterruptedException e) {
//...
Thread.currentThread().interrupt();
}
try {
method2();
} catch (InterruptedException e) {
//...
Thread.currentThread().interrupt();
}
while (!isInterrupted()) {
doSomeThing...
}
}
}

//处理完InterruptedException,重新将线程设为中断状态
static void method0() {
try {
...
} catch (InterruptedException e) {
//...
Thread.currentThread().interrupt();
}
}
//直接抛出
static void method1() throws InterruptedException {
Thread.sleep(5000);
}
//处理完InterruptedException,抛出InterruptedException
static void method2() throws InterruptedException {
try {
...
} catch (InterruptedException e) {
//...
throw e;
}
}

线程停止的正确方式

由于我们不能保证我引用第三方代码能够正确的处理中断,所以并不能完全依靠isInterrupted方法读取中断状态来终止线程。比较保险的方式我们手动添加一个状态标志位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Task extends Thread {

private volatile boolean terminated = false;

@Override
public void run() {
while (!terminated) {
doSomeThing...
}
}

@Override
public void interrupt() {
terminated = true;
super.interrupt();
}
}

记住,不要忽略InterruptedException !