07_03_中断响应

中断响应

在取消任务和中断线程中,中断都起到了很大作用:

  • 取消任务,最合理的方式就是使用中断
  • 中断线程,线程收到中断后作何处理

但是有个问题:

  • 任务和线程都能接收中断请求,那怎么区分?

也就是,中断响应由谁来负责?

一、区分任务取消和线程中断

  • 任务取消和线程中断,是 2 种不同的行为
  • 任务取消的目的是结束任务执行;线程中断的目的是向线程发送中断信号
  • 任务取消的对象是任务;线程中断的对象是线程
  • 中断事件,是从下往上冒泡的,而任务运行在线程内,所以是任务先收到中断,才轮到线程

二、避免任务屏蔽中断

由于是任务先收到中断,如果任务把中断屏蔽了,那后面线程就收不到中断信息了。

所以,不管在何种情况下:

  • 任务收到中断后,无论任务是否响应中断,都不应该清除中断信息
  • 只有实现了中断策略的代码才可以屏蔽中断请求

也就是说,不要在任务中随便屏蔽中断信号。

错误示例:

1
2
3
4
5
6
7
8
public void run() {
try {
// ...
} catch (InterruptedException e) {
// 错误做法
// 不要捕获中断后,什么都不做
}
}

正确做法:

1
2
3
4
5
6
7
8
9
public void run() {
try {
// ...
} catch (InterruptedException e) {
// 正确做法
// 把中断信号恢复回来
Thread.currentThread().interrupt();
}
}

一般来说,如果没有实现中断策略,在收到中断后有2种处理策略:

  • 传递异常:将中断异常抛出,交由调用者处理。比如代码库都是这样做的
  • 恢复中断状态:不想或没办法传递异常时,可以再次调用 interrupt 方法来恢复中断状态

任务只要保证中断信息能留下来即可,至于什么时候恢复,可以自由选择。

比如,可以等所有任务执行完成后,再恢复中断信息:

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
class DelayExitThread extends Thread {

@Override
public void run() {
boolean interrupted = false;
try {
Runnable task;
for(;;) {
try {
// 检查是否发生了中断
if (Thread.currentThread().isInterrupted()) {
// 记录中断信息
interrupted = true;
}

task = getNextTask();
if (task == null) {
break;
}
task.run();
} catch (InterruptedException e) {
// 记录中断信息
interrupted = true;
}
}
}finally {
if (interrupted) {
// 恢复中断信息
Thread.currentThread().interrupt();
}
}
}
}

不同的任务,可能中断处理方式不同,但最终应该都能够保留中断状态才对。

除非确认中断信号已经没用了,否则任务万不可屏蔽中断请求。

三、线程只能由所有者中断

由于每个线程都有各自的中断策略,所以不要随便中断一个未知的线程。

  • 线程只能由其所有者中断,或者知晓中断对线程的含义才可以中断,否则不应该中断该线程

因为不清楚中断策略,就不知道中断线程后会发生什么,会导致什么结果。

比如说,线程在收到中断后立即退出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ExitThread extends Thread {
public void run() {
try {
Runnable task;
final int size = queue.size();
for(int i = 0; i < size; i++) {
task = tasks.take();
task.run();
}
} catch (InterruptedException e) {
// 中断线程
}
}
}

这种情况下,中断后剩余未做的任务就不会再执行了,而这未必是任务提交人所要的结果。

所以,中断前必须先了解线程的中断策略,否则胡乱使用中断可能引发很多不可预料的结果。

四、单独取消任务

既然要把任务取消和线程中断区分开,那么应该怎么区分呢?

虽然都是用中断,但是可以将它们俩的调用方式区分开来。

  • 任务取消:封装一套任务取消的方式,比如 Future.cancel()
  • 线程中断:依旧使用原有的接口 Thread.interrupt()

通过使用不同的方式,就能大致区分它们(注意不是绝对的)。

可以单独封装任务的取消,比如这样做:

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
class CancelableTask implements Runnable {

private final Runnable task;
private final Thread taskThread;

public boolean cancel() {
try {
taskCancel = true;
// 使用中断来取消任务
taskThread.interrupt();
} finally {
}
return true;
}

public void run() {
try {
task.run();
} catch (InterruptedException e) {
// 收到中断请求
} finally {
// 判断是否只是任务取消的中断请求
if (taskCancel) {
// 在任务结束前,清除中断信息
Thread.interrupted();
}
}
}
}

最后这里为什么要清除中断信息?其实目的为了区分任务取消和线程中断。

但这种方式并不完美,本意只是为了清除任务取消引起的中断,但是实际上有可能清除了别的中断:

1
2
3
task.cancel();           // 1

taskThread.interrupt(); // 2

如果同时执行任务取消和线程中断,那么取消(1)就有可能把中断(2)的中断信息给清除了。

实际上,Java 的类库中已经封装好了任务的取消操作,就是 Future.cancel()

Future.cancel() 原理和上面代码类似,不过在最后没有清除中断信息,因为有可能清除掉别的中断。

不过还是建议,取消任务时尽量使用 Future.cancel(),而不要直接用 Thread.interrupt()

总结

  • 区分任务取消和线程中断

    • 任务取消和线程中断,是 2 种不同的行为
    • 任务取消的目的是结束任务执行;线程中断的目的是向线程发送中断信号
    • 任务取消的对象是任务;线程中断的对象是线程
    • 中断事件,是从下往上冒泡的,而任务运行在线程内,所以是任务先收到中断,才轮到线程
  • 避免任务屏蔽中断

    • 任务收到中断后,无论任务是否响应中断,都不应该清除中断信息
    • 只有实现了中断策略的代码才可以屏蔽中断请求
  • 一般来说,如果没有实现中断策略,在收到中断后有2种处理策略:

    • 传递异常:将中断异常抛出,交由调用者处理。比如代码库都是这样做的
    • 恢复中断状态:不想或没办法传递异常时,可以再次调用 interrupt 方法来恢复中断状态
  • 线程只能由其所有者中断,或者知晓中断对线程的含义才可以中断,否则不应该中断该线程

  • 单独取消任务

    • 任务取消:封装一套任务取消的方式,比如 Future.cancel()
    • 线程中断:依旧使用原有的接口 Thread.interrupt()
  • 线程中断实际上依赖于任务的中断处理,如果任务将中断屏蔽了,那么线程将不会收到中断

作者

jiaduo

发布于

2022-05-15

更新于

2023-04-03

许可协议