Spring 里怎样正确处理 InterruptedException?

347 天前
 tnhmcm

公司有一个 Spring Boot 项目, 最近用 Sonar 做了代码扫描,它指出InterruptedException的处理方式不正确。
原代码类似这样:

try {
    CountDownLatch latch = new CountDownLatch(n);
	//...
    latch.await();
} catch (InterruptedException e) {
    log.error("...", e);
    throw new MyAppException("...");
}

Sonar 的解释是,如果你手动捕获中断异常,就得要么重新抛出它,要么手动中断线程,并且在那之前完成必要的清理工作:
If an InterruptedException or a Threadeath error is not handled properly, the information that the thread was interrupted will be lost. Handling this exception means either to re-throw it or manually re-interrupt the current thread by calling Thread.interrupt(). Simply logging the exception is not sufficient and counts as ignoring it. Between the moment the exception is caught and handled, is the right time to perform cleanup operations on the method's state, if needed.

我却对此陷入了疑惑,首先我很少见到在业务代码里直接中断线程的,这应该不是最合适的处理方式。其次,就算一直向上传递InterruptedException,到DispatcherServlet也当成一般异常处理了,那感觉和抛自定义异常是一样的(简单跟了下代码得出的结论,可能不准确),所以我没有看出那样修改的必要。

我的想法是否正确呢?如果有误请纠正我,谢谢大佬们

1979 次点击
所在节点    Java
14 条回复
timethinker
347 天前
https://www.baeldung.com/java-interrupted-exception

这个链接的 3.1 说到: The purpose of the interrupt system is to provide a well-defined framework for allowing threads to interrupt tasks (potentially time-consuming ones) in other threads. A good way to think about interruption is that it doesn’t actually interrupt a running thread — it just requests that the thread interrupt itself at the next convenient opportunity.

我个人理解中断是一种协调机制,一般用来优雅关闭某种耗时操作,当这个异常被抛出来的时候,一般是线程的 Thread.interrupt()方法被调用了,证明有人想要中断当前所进行的任务。处理中断异常有两种选择,一种是恢复中断,不理外界想要中断的意图,继续埋头干活,只需要调用 Thread.interrupted()将线程的中断标记清除掉。另一种就是响应这个中断请求,结束任务并清理相关的资源,或者继续向上传播中断异常。
chendy
347 天前
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 可加可不加的 log
log.error("interrupted", e);
// 然后再次中断
Thread.currentThread().interrupt();
}
ZZ74
347 天前
如楼上 所说 Thread.interrupted() 是标志复位。不影响你 catch 里面继续抛异常,把线程中断往上抛是不合适的,你可以转换成任务被中断导致失败之类的异常。
nothingistrue
347 天前
基本概念 1 ,线程阻塞状态,与同步任务的阻塞性,是两个概念:二者在某些情况下是相似的,但更可能是对立的;譬如,sleep 虽然阻塞当前线程,但减少系统整体上的任务阻塞性(如果当前线程不通过 sleep 、wait 等阻塞自己,那么他将一直占用 CPU ,导致系统整体上的无响应)

基本概念 2 ,中断不是中止:中断只是一个信号,表示请求提前中止;收到中断信号的一方,可以保存状态然后取消资源占用,也可以不予理会。

基本概念 3 ,中断与阻塞不是一个概念的两种叫法,相反,中断是用来取消阻塞的。



现在来看 InterruptedException ,它表示的是:正在阻塞的当前线程被其它线程打断(或者说,收到了其他线程发过来的中断请求,并且做了先期响应);并不是,无法进入阻塞状态。拿 sleep 来说,抛出 InterruptedException 的过程是:已经开始休眠,在预期结束前收到了其他线程的中断信号,抛出 InterruptedException ,同时清除“中断”状态(抛出异常算是对中断信号的先期响应,自然也算是响应过了中断信号)。

InterruptedException 的本意是让当前线程响应中断信号。对于不允许取消的阻塞片段(通常 sleep 是如此的),处理方式应当是报错或者继续抛出。对于允许取消的阻塞片段(通常 wait 、await 是如此的),应当是取消后面依赖该阻塞的片段,例如死循环中可以通过 `continue` 来做取消。

你这个正好是 await ,这时候出现 InterruptedException 是正常的线程通信——根据场景推断,这有可能是外部发起了 cancel 动作,这时候你要做得是,取消后面的任务,然后通过 return 反馈任务已取消状态和任务实际执行的情况。不能通过 throw 来返回任务已取消,因为任务取消是不是异常,是由接受 return 的那一方决定的。当然,如果你的设定上是「任务不能被取消」,那么收到 InterruptedException ,也即取消信号,就可以抛出「不允许被取消的任务,被取消」异常。不过这个「任务不能被取消」的设定本身,可能就要挨批。
nothingistrue
347 天前
需要提醒一下,sleep 的 InterruptedException ,你可能会搜索到让你用 「 Thread.currentThread().interrupt() 」来响应,这个是最后选择的响应手段,不要无脑用。它的实际效果,等同于空的 catch 。

以 sleep 来说,假如 sleep 10 秒,第 5 秒的时候 catch 了 InterruptedException ,如果你只做了 「 Thread.currentThread().interrupt() 」,那么最终的效果是:当前线程设计休眠 10 秒,第 5 秒的时候被其他线程给出了中断信号,当前线程以提前中止休眠(并继续后面的处理),作为中断的响应。
Masoud2023
347 天前
除非流程里设计有专门的应对取消的情况,否则最好的办法就是不处理继续向上传播。
Aresxue
347 天前
尽量不要用 Thread.currentThread().interrupt() ,InterruptedException 其实最好是抛出去,业务代码里除非是为了解耦主流程和辅助流程、批处理循环时隔离单条的失败等情况不建议 catch 异常,这个事情恶心的一个点在于 InterruptedException 是个 unchecked exceptions ,向上抛 java 会强制你修改你的方法签名,如果不想这么干就直接将其包装成一个运行时异常再抛出去类似 throw new PackageException(e)这样,然后在全局异常处理的地方不捕获后 unpackage 再对其做相应处理。
tnhmcm
347 天前
@nothingistrue 好详细的回答,深度学习了。我应该可以这样来总结:中断异常是阻塞方法中去检查中断标志而抛出的,怎么处理要看具体是何操作,是否允许中断,根据不同情况选择抛异常或返回具体执行信息。同时,要谨慎使用 Thread.interrupt(),它只设置中断标志,后续代码若不检查,仍会按正常流程执行(除非涉及数据库或文件等操作,里面会检查)。
tnhmcm
347 天前
@Aresxue 确实,如果位置很多层级又深的话,全加上签名太痛了😩 这么看的话,我这里可以不管 Sonar 这个提示的吧,不管是中断异常还是包装异常,都是对上游做一个异常响应而已。
nothingistrue
347 天前
@tnhmcm #8 关于「 Thread.currentThread().interrupt() 」这块,我说错了,因为我把它当成 interrupted() 去看了,这块不要看了。
Aresxue
347 天前
@Aresxue 有些地方有口误,InterruptedException 是个 checked exception ,翻译成中文是必须要检查( throw or catch )的异常
nothingistrue
347 天前
@tnhmcm 关于 「 Thread.currentThread().interrupt() 」,更正如下。

此方式的设计理念是:当前代码选择直接吃掉 InterruptedException ,但是仍然设置中断信号,以不影响上层可能还有的中断响应。此时,如果上层没有中断响应,那么跟吃掉 InterruptedException 是一样的表现。但如果上层还有中断响应,那么怎么表现就取决于这个中断响应怎么做。

这个仍然是最后才选择的响应方式,首选响应方式,仍然是:如果该阻塞不允许被取消(譬如 sleep 被打断),就抛出业务异常;如果该阻塞允许被取消,那就按照取消去做后续处理,以你发出来这段代码为例子,它应该是清理 latch 并返回它的最新状态给上层调用代码,上层调用代码再自行判断是当成错误还是忽略。

Sonar 这个要求其实有点扯淡,它实际上要求必须按照正常情况去响应 InterruptedException 。但是有些场景,收到 InterruptedException 就是异常情况。场景举例,设计上就是要通过 sleep 来延迟 10 秒后再做后续处理,如果刚 5 秒就有沙雕打断它,那这就是异常情况,就该直接抛出去。
Hashub
346 天前
我是在 cath 里面加了 Thread.currentThread().interrupt();
tnhmcm
346 天前
@nothingistrue 大佬回答很用心,真的非常感谢。处理方式的话,这里因为是实时接口,设计上来说如果出错了,让它报错出去好像也可以。当然,它没考虑到个别任务超长导致 await 一直阻塞的情况,后续我再看怎么调整。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1000185

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX