线程与中断(interrupt)——语义、阻塞点表现与优雅取消
线上最常见的并发事故之一:
- 线程池关不掉
- 任务停不下来
- 超时后仍在后台“偷偷跑”
根因往往不是线程池参数,而是 中断(interrupt)机制没用对。
1. interrupt 的本质:协作式取消(不是强杀)
Thread.interrupt() 做的事情很克制:
- 给目标线程设置一个“中断标志位”(interrupted flag)
- 如果目标线程正阻塞在某些可中断方法上,会让其尽快返回/抛异常
它不会:
- 直接把线程杀死
- 直接回滚业务逻辑
工程含义:
- 想“停得下来”,必须在任务代码里 配合检查中断并尽快退出。
2. 两个 API 必须分清
t.isInterrupted():查询指定线程的中断标志位,不清除。Thread.interrupted():查询 当前线程 的中断标志位,并且 会清除。
常见坑:
- 你在循环里用
Thread.interrupted()当判断条件,结果第一次判断就把标志清了,后面再也感知不到中断。
3. 常见阻塞点遇到 interrupt 的表现
3.1 会抛 InterruptedException(并清除标志)
Object.wait()Thread.sleep()Thread.join()BlockingQueue.put/take()等阻塞队列方法ReentrantLock.lockInterruptibly()
结论:
- 捕获
InterruptedException后,如果你要把“中断语义”往上层传递,通常要:Thread.currentThread().interrupt();(回填标志)
3.2 不抛 InterruptedException(但会让你自己处理)
LockSupport.park():不会抛异常,但会因为中断而返回;通常你需要Thread.interrupted()/isInterrupted()自己判断
3.3 可能完全不响应
- 不可中断的阻塞(取决于具体 API)
- 卡在 native IO/某些同步原语上
工程建议:
- 关键任务尽量使用可中断 API 或可超时 API(
tryLock(timeout)、get(timeout))。
4. 优雅取消的“标准模板”
4.1 循环任务:定期检查中断
1 | public void run() { |
4.2 处理中断异常:回填标志 + 退出
1 | public void run() { |
这是很多面试题的标准答案:
- catch 后回填,否则上层看不到中断。
5. 在线程池里“取消”一个任务:Future.cancel(true)
Future.cancel(true) 做了两件事:
- 把 Future 标记为取消
- 如果任务正在运行,尝试对执行它的线程
interrupt()
注意:
- 如果任务代码不配合(不检查中断、或卡在不可中断点),取消并不会立即生效。
工程建议:
- 对外提供超时/取消语义的任务,内部必须按第 4 节模板写。
6. 线程状态与排障(定位“卡住的线程”)
线程常见状态:
RUNNABLE:可能在跑,也可能在等待 IO(native)BLOCKED:在等synchronizedmonitorWAITING/TIMED_WAITING:在 wait/join/park/sleep
排障思路(结合线程名):
- 大量
BLOCKED:锁竞争/锁顺序问题 - 大量
WAITING (parking):AQS/LockSupport 阻塞,可能线程池耗尽或下游慢 - 大量
TIMED_WAITING:sleep/backoff/超时等待
7. 总结
- 中断不是“强杀”,而是“请求停止”。
- 能不能停下来,取决于你的任务代码有没有按规范:
- 可中断阻塞点
- 轮询检查中断
- 捕获中断异常后回填标志
下一篇等待与唤醒机制会把:wait/notify、Condition、LockSupport 三套机制放在一张对比表里讲清楚,并解释“为什么会虚假唤醒/信号丢失”。
面试题 / Checklist
interrupt()做了什么?为什么说它是“协作式取消”而不是强杀?isInterrupted()与Thread.interrupted()的差别是什么?后者为什么危险?- 哪些阻塞点会抛
InterruptedException?捕获后为什么通常要回填中断标志? LockSupport.park()在中断时表现如何?为什么仍要循环检查条件?Future.cancel(true)与线程中断之间是什么关系?如何确保任务“停得下来”?
