news 2026/11/3 10:45:02

第 9 集:任务超时了,为什么还在运行?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
第 9 集:任务超时了,为什么还在运行?

资深 Java 面试到 Agent 开发工程师 · 第 9 集

我自己做了一个 Java 并发小游戏《并发突围》,无广告、免费玩。点这里试玩。

接口返回“查询超时”,后台却还占着线程。原因是:get(timeout)只结束等待,不会自动取消任务。要让工作停下来,需要取消实际任务、让任务响应,并执行资源清理。

1. 超时以后,任务还会继续执行

用休眠两秒模拟耗时查询,调用方只等 100 毫秒。导入java.util.concurrent.*,把代码放进声明了throws Exception的main方法中:

ExecutorServicepool=Executors.newSingleThreadExecutor();try{Future<String>future=pool.submit(()->{Thread.sleep(2000);return"done";});try{future.get(100,TimeUnit.MILLISECONDS);}catch(TimeoutExceptione){System.out.println("等了 100 毫秒,还没有结果");}System.out.println(future.get());// 继续等,最终得到 done}finally{pool.shutdownNow();}

main线程等结果,工作线程执行休眠。100 毫秒内没结果,main收到TimeoutException,工作线程仍会继续休眠。因此,第二次不带超时的get()还能等到done;它只是用来观察结果,实际接口不必继续等。

结果已经不需要时,可以在超时后申请取消:

try{returnfuture.get(1,TimeUnit.SECONDS);}catch(TimeoutExceptione){future.cancel(true);throwe;}

这段只展示等待超时的分支。调用方若收到InterruptedException,也应按任务归属申请取消,再向上抛出异常或恢复中断标记。true表示允许尝试中断,任务需要配合。如果多个请求共用同一次查询,不能因其中一个超时就取消共享任务。

2. 收到中断以后,任务怎样退出

线程池submit通常返回 FutureTask。取消成功时,cancel(true)尝试中断工作线程。interrupt()会设置中断标记,任务检查它,或在可中断等待中处理异常。

下面的代码会忽略中断,继续循环:

while(true){try{Thread.sleep(1000);}catch(InterruptedExceptione){// 只记日志,然后继续循环}}

sleep被中断时会抛出InterruptedException,并清除中断标记。这个catch在循环里面,处理完又睡下一轮。把它移到循环外,并检查标记:

try{while(!Thread.currentThread().isInterrupted()){Thread.sleep(1000);// 执行一小段工作}}catch(InterruptedExceptione){Thread.currentThread().interrupt();}finally{// 释放任务自己持有的资源}
中断发生在哪里这段代码怎样退出
正在sleep不必睡满一秒,抛出异常,进入循环外的catch,随后执行finally
检查循环条件之前isInterrupted()返回true,结束循环,执行finally

catch中的interrupt()恢复被清除的标记;恢复标记本身不会结束循环。方法允许抛出InterruptedException时,也可以交给调用者处理。

finally要写实际清理代码,例如关闭独占文件、归还借用的连接。清理完成、任务返回后,工作线程才能处理下一项任务;清理本身也可能耗时。

若调用方必须知道已开始的任务何时结束清理,可以单独使用CountDownLatch:任务在清理路径结束时调用countDown(),等待方用带超时的await()检查是否收到通知。通知只表示清理路径已走完;清理是否成功,还要单独记录结果。

上面的清理路径要求任务已经进入任务体。若任务仍在队列里就被取消,任务体和其中的finally都不会运行,也不会从那里发出“清理完成”的通知。任务内部才借用的资源尚未占用;提交前已取得的名额或资源,要由持有者协调归还。启动与取消可能同时发生,不能仅凭一次isDone()判断由谁清理。

一直计算的任务不会自动抛中断异常,需要主动检查:

for(inti=0;i<items.size();i++){if(Thread.currentThread().isInterrupted()){return;}process(items.get(i));}

这段代码每项处理前检查一次。如果一次process卡住很久,也得等它返回才能再次检查。卡在网络或数据库调用里,还要用对应客户端支持的取消与超时机制。

3. 为什么 isDone 为 true,任务却还没退出

Future 的“完成”描述结果状态。FutureTask 取消成功后,isDone()、isCancelled()都为true,再get()会抛CancellationException,任务代码却可能继续运行:

OpenJDK 21 的实现如下。state记录 Future 状态,runner保存执行线程,参数mayInterruptIfRunning就是传入的true或false:

publicbooleancancel(booleanmayInterruptIfRunning){if(!(state==NEW&&STATE.compareAndSet(this,NEW,mayInterruptIfRunning?INTERRUPTING:CANCELLED)))returnfalse;try{if(mayInterruptIfRunning){try{Threadt=runner;if(t!=null)t.interrupt();}finally{STATE.setRelease(this,INTERRUPTED);}}}finally{finishCompletion();}returntrue;}
源码位置做了什么
state == NEW只有状态尚未被完成或取消修改,才继续尝试。任务已经运行时,也可以是NEW
STATE.compareAndSetSTATE用来操作state。原子地检查并修改状态,别人抢先完成或取消,这次就返回false
INTERRUPTING→runner.interrupt()→INTERRUPTED开始发送中断、通知执行线程、结束发送流程。这里没有等待任务退出
finishCompletion()唤醒等结果的线程,清理 Future 内部等待记录;不替任务关闭业务资源

传入false时不发中断。cancel(false)也可能成功,而已经开始的任务继续执行。取消状态和资源是否已清理,要分别确认。

4. 接口超时后,怎样取消查询、释放资源

先区分几个名字相近的操作:

操作能做什么是否会中断已运行的任务
Future.get(timeout)限制这次等结果的时间不会
FutureTask.cancel(true)改为取消状态,尝试中断执行线程会尝试,任务要响应
普通CompletableFuture.cancel(true),如supplyAsync返回值把结果设为取消异常不会,true在基类实现中不起作用
JDK 21 默认HttpClient.sendAsync返回的 Future,调用cancel(true)尝试取消实际 HTTP 请求由客户端取消网络操作,不是靠中断某个计算线程
CompletableFuture.orTimeout(...)让结果以超时异常完成,Java 9 起提供不会

普通 CompletableFuture 的基类实现如下;客户端定制的返回值,需要另看其约定:

publicbooleancancel(booleanmayInterruptIfRunning){booleancancelled=(result==null)&&internalComplete(newAltResult(newCancellationException()));postComplete();returncancelled||isCancelled();}

result保存结果,AltResult包装取消异常,internalComplete尝试记录它,postComplete处理后续操作的完成。这里没有调用执行线程的interrupt()。例如,前面取消后,thenApply返回的 Future 也会异常完成,不再执行其中的函数。

实际查询应保留客户端的取消入口,或执行它的任务 Future。JDK 21 默认 HTTP 客户端对返回值定制了取消行为,所以不能只看变量类型;普通supplyAsync没有这项请求取消能力。对于已开始、仅服务本次请求的 FutureTask,取消成功后的流程如下:

查询交给 HTTP 或数据库客户端时,还要配置它支持的操作超时,必要时使用请求取消方法。是否中断读写、归还连接,要核对具体客户端约定;任务返回也不等于远端已经停止。清理只针对本任务持有的资源,不关闭共享连接池。

修改后,在测试环境让一次查询变慢,用同一个请求 ID 关联超时、取消、查询结束和清理记录:

观察到什么接着检查哪里
接口超时,但没有取消记录是否保存并取消了实际任务的 Future
取消成功,查询却一直没结束是否吞掉中断;客户端是否支持、启用了取消或超时
查询结束,连接仍未归还响应体/连接的关闭及归还逻辑
排队任务已取消,队列却没变短普通线程池可能还没移除包装对象;按需检查remove/purge,不必每次超时都扫描队列

如果底层调用没有中断、取消或有效超时支持,需要修改或更换调用方式。准备重试远端写入时,还要确认原请求结果或使用幂等设计;本地取消不能撤回已完成的写入。

不要用Thread.stop()强行结束查询。旧实现可能在共享数据只更新了一部分时释放锁,其他线程随后就会看到不完整状态;JDK 21 调用它会直接抛出UnsupportedOperationException。让任务在可退出的位置响应中断,才能执行上面讨论的清理流程。

5. 旧任务没停,新任务又来了

慢查询占着线程,新请求不断提交,队列就会积压。可以限制排队数量:

ThreadPoolExecutorpool=newThreadPoolExecutor(2,2,0L,TimeUnit.MILLISECONDS,newArrayBlockingQueue<>(20),newThreadPoolExecutor.AbortPolicy());

2 和 20 只是容量示例,实际按下游承载、耗时与压测结果选择;服务中复用由应用管理的线程池。这个配置最多 2 项执行、20 项排队,满后抛RejectedExecutionException。线程池入队使用offer,队列满就失败,不会等空位。接收后的超时检查与拒绝后的处理如下:

图中 1 秒是整个请求的假设预算,包含排队。排队用了 800 毫秒,查询只剩约 200 毫秒,应使用客户端支持的总调用超时或截止时间。只把readTimeout设为 200 毫秒,不一定限制整次调用;例如 OkHttp 4.12 的读取超时针对单次读等待,callTimeout才覆盖整次调用及其中的重试。任务已过期就跳过,并让等待方收到超时结果。

措施限制什么怎么用在这个查询中
有界队列最多积压多少任务排队位置满了就拒绝,调用方处理繁忙
背压根据处理速度放慢提交后台每批处理完再读取下一批,每批数量有上限
速率限制每秒接收多少请求超出允许速率就拒绝或按约定等待
并发限制同时有多少查询在执行下游变慢时,仍限制同时调用的数量

例如每秒仍进来 10 个请求,查询从 100 毫秒变成 2 秒,未结束的查询会变多。因此,只限制请求速率还不够。并发名额若代表实际查询,就应在查询真正结束后归还,不能因调用方超时便提前释放。拒绝后立即反复重试,也会继续增加压力。

CallerRunsPolicy能让提交线程自己执行任务,从而减慢提交;但多个请求线程可能同时这样做,加上池内任务,查询并发就可能超过 2。它不能当作下游并发上限。

线程池的关闭是应用生命周期操作,不是单个请求的取消方式。shutdown()拒绝新任务,并让已提交任务继续执行;shutdownNow()尝试中断正在执行的任务,并返回尚未开始的排队任务。返回排队任务并不等于自动取消它们对应的所有 Future:持有者仍要处理这些任务,避免调用方一直等待。两种方法都不等待池完全退出;需要等待时,可以用有超时的awaitTermination()检查结果。

常见面试题怎么答?

怎样停止一个正在运行的线程?为什么不直接用 stop?

让任务响应中断,或检查有可见性保证的停止标志,再退出并清理。volatile停止标志可用于循环检查,但不能叫醒正在阻塞的线程。可中断等待则能通过InterruptedException响应。

旧版Thread.stop可能在线程只改了一半共享数据时强行终止,并释放锁,让其他线程看到不完整数据,因此被废弃。JDK 21 会直接抛UnsupportedOperationException。任务自行退出才能安排清理;卡在不响应取消的操作里,也暂时走不到finally。

Future.cancel(true) 和 CompletableFuture.cancel(true) 一样吗?

不一样。Future 接口允许实现尝试中断;FutureTask 会这样做,任务要响应。普通 CompletableFuture 通过取消异常完成结果,不中断已开始的计算;客户端可以定制取消行为,例如 JDK 21 默认HttpClient.sendAsync返回值能尝试取消 HTTP 请求。不能只凭类型名或isDone()判断底层工作和资源状态。

shutdown 和 shutdownNow 有什么区别?

shutdown()拒绝新任务,让已提交任务继续执行。shutdownNow()尝试中断执行中的任务,并取出、返回未开始的排队任务;这些任务对应的 Future 不会自动全被设为取消,需要处理,否则等待者可能一直等。

两者都不等所有任务结束再返回。关闭后,带超时的awaitTermination返回true才表示整个池已终止。单个请求超时应取消它的任务,不应关闭其他请求共用的线程池。

读完自测

  1. cancel(true) 成功,isDone() 也为 true。能立刻把任务正在用的资源交给别人吗?

    参考要点:不能。对已开始的任务,可在finally清理后用CountDownLatch通知,等待方用带超时的await,并检查清理结果。若开始前就取消,任务体和finally都不会执行,需要单独处理未启动状态。取消后的get()不能用来等清理完成。

  2. 总超时 1 秒,排队用了 900 毫秒,客户端读取超时还设 1 秒,有什么问题?

    参考要点:排队已消耗预算,读取超时也不等于整个调用超时。应按同一截止时间算剩余时间,配合客户端支持的总调用超时或取消,已过期就跳过。获取连接、连接、读写和重试是否计入,要按客户端约定核对。

  3. 队列改成二十,满了就立即重试,能解决积压吗?

    参考要点:不能。小队列只限制池中排队量,立即重试会增加提交。需要处理拒绝、限制并发,并约定次数有限的延迟重试;后台批处理可等本批完成再读下一批。

参考资料

  • Future:等待、取消与完成状态
  • Thread:中断和 stop 的约定
  • CompletableFuture:取消与超时
  • ThreadPoolExecutor:排队、拒绝和关闭
  • JDK 21 HttpClient:中断与请求取消
  • OkHttp 4.12:读取超时与总调用超时
  • OpenJDK 21 FutureTask 源码
  • OpenJDK 21 CompletableFuture 源码

源码按 OpenJDK 21(jdk-21+35)说明;文中 Java 8 可用的示例在 Zulu 8 上验证。orTimeout从 Java 9 起可用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/3 10:44:37

深入探索 Python 核心函数与模块:从 ord() 到 dis

在编程这样的环境当中, 了解语言里面的核心函数还有模块这些内容, 确实能够让咱们的编程工作变得更为高效, 同时也能让代码的质量有明显提高, 这篇文章会把重点放在几个比较重要的函数以及模块上面去讨论, 具体来说就包括了ord这个函数、id这个函数还有dis这个模块, 并且还会利…

作者头像 李华
网站建设 2026/11/3 10:44:35

FindStar星图计划:STM32从一颗 LED 开始

星图 C8T6 01&#xff5c;从一颗 LED 开始&#xff1a;验证整条链路星图计划 C8T6 卷第 1 颗星。FindStar 寻星工作室 STM32F103C8T6 65 个项目&#xff0c;从这颗灯开始。为什么第一课永远是点灯 每个学单片机的人都被同一句话送进门&#xff1a;先让灯闪起来。 这话听起来…

作者头像 李华
网站建设 2026/11/3 10:43:36

aws(SAA/SAP边学边练)w1d2(S3 基础 + 存储类)

文章目录 aws(SAA/SAP边学边练)w1d2(S3 基础 + 存储类) 1. 先把 6 类存储类"一句话定位"记住 1.1 核心速查表 1.2 两个必考补充点 1.3 选择标准 2.动手实操 2.1 动手实操 1:创建桶 + 上传 + 切换存储类(CLI) 2.1.1 创建桶(桶名全局唯一,注意区域要跟 CLI 默认一…

作者头像 李华
网站建设 2026/11/3 10:43:36

【Python】五大容器

列表为什么需要列表如果要存储多个同类数据&#xff08;比如全班同学名字&#xff09;&#xff0c;单独定义一个个变量非常麻烦&#xff0c;列表可以一次性存放多个数据&#xff1b;其他语言里叫数组&#xff0c;Python 里叫 list&#xff0c;列表是可变类型列表定义语法&#…

作者头像 李华
网站建设 2026/11/3 10:41:49

WINDOWS11常见ERROR5问题排查笔记

微软电脑管家&#xff08;ahflt.sys&#xff09;导致 MSI 安装/卸载失败 Error 5 的完整排查实录症状千奇百怪&#xff0c;根因只有一个&#xff1a;微软电脑管家的 AntiHack 过滤驱动拦截了 Windows Installer 的回滚文件权限操作。本文记录从"Python 卸载报错"一路…

作者头像 李华
网站建设 2026/11/3 10:41:48

那些隐藏在系统深处的 Python 运行秘技,高手都在用

“空谈无益&#xff0c;给我看代码。” — Linus Torvalds把一段临时检查命令&#xff0c;写成可解释、可审查、不会顺手泄露配置的自动化脚本。 python3 -I -c with open("/path/to/repo/config/.env") as f:content f.read()print(content) 这段命令很像深夜排障…

作者头像 李华