原创

CompletableFuture 超时后,后台任务为什么还在跑

orTimeout() 已经触发,接口也返回了降级结果,线程池却还在忙。这个现象并不矛盾:Future 可以先结束,执行它的工作仍然继续。

Java 9 引入的 CompletableFuture.orTimeout() 让结果在到期时以 TimeoutException 完成。它没有取得底层任务的中断句柄,也不会替 HTTP 客户端关闭连接。把结果状态当成资源状态,正是问题的起点。下面以 JDK 17 说明。

用两个 Future 分开观察

例子用闸门阻塞工作线程,不依赖任务“差不多睡了多久”的猜测。代码可放在允许抛异常的测试方法中,常规 import 省略。

ExecutorService pool = Executors.newSingleThreadExecutor();
CountDownLatch release = new CountDownLatch(1);
CompletableFuture<String> source = CompletableFuture.supplyAsync(() -> {
    try {
        release.await();
        return "work finished";
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new CompletionException(e);
    }
}, pool);

try {
    CompletableFuture<String> caller = source.copy()
            .orTimeout(100, TimeUnit.MILLISECONDS);
    try {
        caller.join();
    } catch (CompletionException e) {
        System.out.println(e.getCause().getClass().getSimpleName());
    }
    System.out.println(source.isDone()); // 闸门还没打开,仍为 false
} finally {
    release.countDown();
    source.join();
    pool.shutdown();
}

输出中的 TimeoutException 属于 caller,source.isDone() 仍是 false。打开闸门以后,原始工作才返回。finally 中释放闸门并关闭线程池,是为了让演示程序自身也能正常退出。

copy() 值得单独看一眼。orTimeout() 返回的是被调用的那个 Future,本身不是创建隔离视图的操作。如果直接对共享的 source 调用它,所有依赖 source 完成状态的消费者都可能看到同一个超时。对单个调用方使用副本,可以隔离它的等待期限,却仍然不会复制或取消那次后台计算。

CompletableFuture 超时后,后台任务为什么还在跑的流程示意

completeOnTimeout() 的区别是到期补一个正常值,底层资源问题照旧。CompletableFuture.cancel(true) 中的 true 也不是“工作线程已中断”的证据;该类不使用这个参数控制执行中的计算。

超时预算要落到连接和任务上

假设请求总预算为 500 毫秒,前面取配置已经用了 100 毫秒。随后给每次下游调用重新配置 500 毫秒,并不能保证整个接口在预算内结束。并发包装层、连接池等待、建连和响应读取,各自都有可能花时间。

可行的做法是传递一个明确的截止时刻,在进入实际调用前计算剩余预算。HTTP 客户端负责请求超时与取消;数据库访问检查驱动和查询超时的实际行为;自写循环则在合适位置检查停止标识或中断。能否取消,最后取决于实际执行操作的组件。

Future.cancel(true) 也只有在持有对应任务句柄、执行器支持中断、任务愿意响应中断时,才可能帮助回收线程。不能因为换了一个 Future 类型,就承诺任意第三方阻塞代码会立刻退出。

线程池的边界同样重要。请求 300 毫秒就返回,后台调用却占用线程 30 秒,系统会同时积压多个已经无人等待的工作。继续往无界队列里提交,只会把超时变成排队问题。队列长度、活动线程和实际下游完成时间,比单看接口 P99 更能说明这批资源到底什么时候释放。

写请求不能凭超时决定重发

超时只说明没有在约定时间内拿到结果。下游可能已经完成扣款或创建订单,只是响应没有及时回来。此时自动重试会把等待问题扩大成业务重复。

读请求可以按容量预算考虑取消与重试;写请求必须先确定幂等键、重复请求返回方式和结果查询入口。若调用方已经超时返回,但工作稍后成功,也要有可观察的结果去向。Future 的完成状态适合组织程序控制流,不能替代业务状态机。

正文到此结束
评论插件初始化中...
Loading...