首页 / 资讯中心 / 文章详情

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

第 9 集:任务超时了,为什么还在运行? ★ FEATURED ARTICLE
资深 Java 面试到 Agent 开发工程师 · 第 9 集我自己做了一个 Java 并发小游戏《并发突围》无广告、免费玩。点这里试玩。接口返回“查询超时”后台却还占着线程。原因是get(timeout)只结束等待不会自动取消任务。要让工作停下来需要取消实际任务、让任务响应并执行资源清理。1. 超时以后任务还会继续执行用休眠两秒模拟耗时查询调用方只等 100 毫秒。导入java.util.concurrent.*把代码放进声明了throws Exception的main方法中ExecutorServicepoolExecutors.newSingleThreadExecutor();try{FutureStringfuturepool.submit(()-{Thread.sleep(2000);returndone;});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结束循环执行finallycatch中的interrupt()恢复被清除的标记恢复标记本身不会结束循环。方法允许抛出InterruptedException时也可以交给调用者处理。finally要写实际清理代码例如关闭独占文件、归还借用的连接。清理完成、任务返回后工作线程才能处理下一项任务清理本身也可能耗时。若调用方必须知道已开始的任务何时结束清理可以单独使用CountDownLatch任务在清理路径结束时调用countDown()等待方用带超时的await()检查是否收到通知。通知只表示清理路径已走完清理是否成功还要单独记录结果。上面的清理路径要求任务已经进入任务体。若任务仍在队列里就被取消任务体和其中的finally都不会运行也不会从那里发出“清理完成”的通知。任务内部才借用的资源尚未占用提交前已取得的名额或资源要由持有者协调归还。启动与取消可能同时发生不能仅凭一次isDone()判断由谁清理。一直计算的任务不会自动抛中断异常需要主动检查for(inti0;iitems.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或falsepublicbooleancancel(booleanmayInterruptIfRunning){if(!(stateNEWSTATE.compareAndSet(this,NEW,mayInterruptIfRunning?INTERRUPTING:CANCELLED)))returnfalse;try{if(mayInterruptIfRunning){try{Threadtrunner;if(t!null)t.interrupt();}finally{STATE.setRelease(this,INTERRUPTED);}}}finally{finishCompletion();}returntrue;}源码位置做了什么state NEW只有状态尚未被完成或取消修改才继续尝试。任务已经运行时也可以是NEWSTATE.compareAndSetSTATE用来操作state。原子地检查并修改状态别人抢先完成或取消这次就返回falseINTERRUPTING→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(resultnull)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. 旧任务没停新任务又来了慢查询占着线程新请求不断提交队列就会积压。可以限制排队数量ThreadPoolExecutorpoolnewThreadPoolExecutor(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才表示整个池已终止。单个请求超时应取消它的任务不应关闭其他请求共用的线程池。读完自测cancel(true) 成功isDone() 也为 true。能立刻把任务正在用的资源交给别人吗参考要点不能。对已开始的任务可在finally清理后用CountDownLatch通知等待方用带超时的await并检查清理结果。若开始前就取消任务体和finally都不会执行需要单独处理未启动状态。取消后的get()不能用来等清理完成。总超时 1 秒排队用了 900 毫秒客户端读取超时还设 1 秒有什么问题参考要点排队已消耗预算读取超时也不等于整个调用超时。应按同一截止时间算剩余时间配合客户端支持的总调用超时或取消已过期就跳过。获取连接、连接、读写和重试是否计入要按客户端约定核对。队列改成二十满了就立即重试能解决积压吗参考要点不能。小队列只限制池中排队量立即重试会增加提交。需要处理拒绝、限制并发并约定次数有限的延迟重试后台批处理可等本批完成再读下一批。参考资料Future等待、取消与完成状态Thread中断和 stop 的约定CompletableFuture取消与超时ThreadPoolExecutor排队、拒绝和关闭JDK 21 HttpClient中断与请求取消OkHttp 4.12读取超时与总调用超时OpenJDK 21 FutureTask 源码OpenJDK 21 CompletableFuture 源码源码按 OpenJDK 21jdk-2135说明文中 Java 8 可用的示例在 Zulu 8 上验证。orTimeout从 Java 9 起可用。
阅读完成 · 觉得有帮助?
咨询建站