任务取消
任务取消让调用方通知后台任务结束工作,并协调任务退出和资源释放。中断传递停止请求,超时限定等待时间,取消则改变任务的结果状态。本文先讲调用链的时间预算,再说明中断和 Future 取消。最后讨论资源清理,以及完成与取消同时发生时的处理。
1. 面试核心
先记住这几条,再读正文- 统一截止时间。 调用链在入口计算统一截止时间,后续每一步只使用剩余预算。
- 等待超时。
Future.get(timeout, unit)超时只结束本次等待,任务可以继续运行。需要终止任务时,还要发出取消请求并由任务响应。 - 中断取消。
FutureTask.cancel(true)成功后将结果标为取消,并尝试中断正在执行的任务。 - 外部副作用。 Future 进入取消状态后,已发出的网络请求仍可在下游产生副作用。卡 · 截止时间与剩余预算
- 不请求中断的取消。
FutureTask.cancel(false)成功后,已经开始的计算可以继续运行,但返回值会被丢弃。卡 · 非中断取消与任务状态
2. 截止时间与剩余预算
调用链需要共同遵守一个等待上限。如果三个步骤各自等待 1 秒,总等待就会接近 3 秒。入口计算统一截止时间后,后续步骤只使用尚未消耗的时间。
Result awaitResult(long deadline)
throws InterruptedException, ExecutionException, TimeoutException {
long remaining = deadline - System.nanoTime();
if (remaining <= 0) {
throw new TimeoutException();
}
return future.get(remaining, TimeUnit.NANOSECONDS);
}
入口用 System.nanoTime() + budgetNanos 计算截止时间。示例在等待前减去当前时间,得到剩余预算。预算已耗尽时立即抛出异常,否则把剩余时间交给 Future.get()。这样,每一层的等待都会消耗同一份预算。
nanoTime() 用来测量时间间隔,不受系统日期和时间校准影响。预算换算还需限制到 long 可表达的合理范围,避免超大时长造成溢出。
3. 中断与可中断等待
中断是协作式的停止信号,需要任务主动配合退出。阻塞方法通过 InterruptedException 响应时,任务可以把异常继续抛给调用方。如果任务边界需要返回业务结果,就恢复中断标志,再结束当前任务。
catch (InterruptedException exception) {
Thread.currentThread().interrupt();
return Result.cancelled();
}
这段代码先恢复中断标志,再返回取消结果。因此,上层仍能观察到中断请求,当前任务也会及时退出。恢复标志后继续长时间执行业务,会拖延线程池关闭或请求取消。
sleep、wait、join 等阻塞方法可通过中断提前结束等待。各 API 的具体响应差异见 Java 线程。
4. FutureTask 的中断取消
FutureTask.cancel(true) 同时涉及结果状态和执行线程。取消成功后,Future 的结果被标为取消。尚未开始的计算将跳过执行,正在运行的计算则收到中断请求。
任务忽略中断,或阻塞在不响应中断的外部调用上时,计算仍会继续。因此,数据库和网络客户端还需配置连接、读取和请求超时。取消结果也不会回滚已经发出的扣款请求。调用方需要用幂等键查询操作状态,再决定重试或补偿。
CompletableFuture 的取消只表达异常完成。它的差异见 CompletableFuture。
5. 资源清理
任务退出时,要释放已经取得的锁、许可和连接。正常完成、异常、中断和超时都应经过相同的清理路径。
Permit permit = pool.acquire();
try {
return doWork();
} finally {
permit.close();
}
示例在获取成功后进入 try。正常返回或抛异常时,finally 都会释放资源。获取过程失败时,无需归还未取得的许可。
如果任务只取得了部分资源,就只释放已经取得的那部分。每个资源都要明确由谁清理,避免遗漏或重复归还。
有些操作必须完成一组更新,才能恢复业务状态的一致性。这时可以先记录取消请求,等这组更新结束后再退出。取消协议还应约定最迟响应时间,让调用方知道还要等待多久。
6. 完成与取消的竞争
任务恰好完成时,调用方也可能正在执行 cancel()。哪一方先完成状态转换,决定了 Future 最终保存正常结果还是取消状态。因此,调用方应检查取消的返回值和最终状态,再执行相应处理。
同一任务的回调、指标和业务状态更新需要去重,避免重复通知造成重复处理。结果状态和外部副作用也要分别确认。比如,Future 已被取消时,扣款请求仍可能已经提交。此时应通过幂等键查询扣款结果,再决定是否重试。
7. 非中断取消与任务状态
FutureTask.cancel(false) 用于取消结果,同时保留正在执行的计算。这里的 false 表示此次取消不请求中断。取消若先完成状态转换,此后的 get() 就会抛出 CancellationException。
计算尚未开始时,成功取消会阻止其执行。计算已经开始时,参数 false 保留其执行线程。任务最终返回的值也不会成为 Future 的正常结果。
isDone() 对正常、异常和取消完成都返回 true,所以它只能表明 Future 已结束。需要确认底层计算和资源清理已经结束时,应由任务另行发出完成信号。
8. 常见问题
8.1 get 超时后,任务会自动停止吗?
不会,get 超时只向等待者抛出 TimeoutException。调用方结束等待后,后台任务仍可继续运行。需要停止任务时,应显式取消,并确认任务会响应中断或客户端超时。
8.2 为什么要单独处理 InterruptedException?
InterruptedException 传递线程的取消信号。把它和普通异常一起吞掉,会让上层无法继续响应取消。例如,线程池关闭后,任务还在执行业务。调用方应继续抛出异常,或恢复中断标志后结束当前任务。
9 · 面试题卡片
9.1 怎样设计一个支持超时和取消的并发任务
出现公司:阿里巴巴、美团
考察重点
- 能说明每一步怎样计算剩余预算,避免各层超时累加。
- 能解释中断怎样传递,以及 Future 取消后任务为什么仍会运行。
- 能说明退出时怎样释放资源,以及如何确认外部操作的结果。
参考回答
任务应共用截止时间,通过中断响应取消,并在退出路径释放资源。
入口先计算截止时间,每次阻塞前再计算剩余预算。计算循环定期检查中断,阻塞等待则使用可中断且带超时的 API。
超时后,调用方用 FutureTask.cancel(true) 发出取消请求。任务在能保持业务一致性的位置退出。捕获 InterruptedException 后,继续抛出或恢复标志后结束任务。
资源通过 finally 或自动关闭机制释放,底层客户端另设连接、读取和请求超时。需要确认清理结束时,由任务发送独立完成信号。对于已发出的外部请求,还需用幂等键查询结果,必要时补偿。
9.2 运行中的 FutureTask 执行 cancel(false) 会发生什么?
出现公司:暂无已核验的公开来源(本站自测)
考察重点:能区分 Future 的结果已取消和执行线程已退出,并说明怎样分别确认。
相关内容:非中断取消与任务状态。
参考回答
取消成功后,Future 进入取消状态,已经运行的计算可以继续执行。
因为 false 表示此次取消不请求中断,任务仍会按原逻辑运行。
此后的 get() 抛出 CancellationException,任务返回的值会被丢弃。isDone() 为 true 也只说明 Future 已结束。若要确认计算和资源清理都已完成,需要任务另行发送完成信号。