You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用CompletableFuture时ExecutorService无法正常关闭的问题排查

问题分析与解决方案

首先,咱们来拆解下你遇到的问题:用自定义ExecutorService执行CompletableFuture任务后,调用shutdown()无法正常关闭,还导致后续请求阻塞,但移除关闭代码就正常。核心原因有两个:

  1. 你只等待了原始异步任务完成,没等待回调任务:CompletableFuture.allOf()只等待了三个supplyAsync的任务,但你后续的thenApply回调是依赖这些任务的新异步任务,allOf()并没有等待这些回调执行完毕就触发了shutdown()。这时候ExecutorService可能还有回调任务在运行,shutdown()会等待这些任务完成,但如果你的控制器没等所有任务结束就返回,就会导致线程池的关闭逻辑和后续请求产生冲突。

  2. 如果ExecutorService是在请求方法内创建的,频繁创建+未正确关闭会引发资源问题:每次请求创建新线程池,不关闭会导致内存泄漏;但关闭时机不对,又会阻塞后续任务。


正确的解决步骤

方案1:等待所有任务(包括回调)完成后再关闭线程池

你需要把thenApply返回的CompletableFuture也加入到allOf()的等待列表中,确保所有回调执行完毕后再关闭线程池:

// 原始异步任务
CompletableFuture<IStoryDetail> iStoryDetailCompletableFuture = CompletableFuture.supplyAsync(() -> storyRepository.getStoryDetails(id), executorService);
CompletableFuture<List<Comment>> iCommentFuture = CompletableFuture.supplyAsync(() -> commentRepository.getComments(id), executorService);
CompletableFuture<List<Images>> iImageFuture = CompletableFuture.supplyAsync(() -> imageRepository.getImagesByStoryId(id), executorService);

// 捕获回调任务的Future
CompletableFuture<Void> storyCallbackFuture = iStoryDetailCompletableFuture.thenApply(story -> objectMap.putIfAbsent(story.getClass().getName(), story))
        .thenRun(() -> {}); // 转为Void类型方便allOf处理
CompletableFuture<Void> commentCallbackFuture = iCommentFuture.thenApply(comments -> objectMap.putIfAbsent(Comment.class.getName(), comments))
        .thenRun(() -> {});
CompletableFuture<Void> imageCallbackFuture = iImageFuture.thenApply(images -> objectMap.putIfAbsent(Images.class.getName(), images))
        .thenRun(() -> {});

// 等待所有任务(原始+回调)完成后关闭线程池
CompletableFuture.allOf(storyCallbackFuture, commentCallbackFuture, imageCallbackFuture)
        .thenRun(() -> {
            executorService.shutdown();
            // 可选:强制等待线程池关闭,避免资源泄漏
            try {
                if (!executorService.awaitTermination(30, TimeUnit.SECONDS)) {
                    executorService.shutdownNow();
                }
            } catch (InterruptedException e) {
                executorService.shutdownNow();
                Thread.currentThread().interrupt();
            }
        });

方案2:将ExecutorService定义为Spring Bean(推荐)

在Spring环境下,不要每次请求创建新线程池,而是把它注册为单例Bean,由Spring管理生命周期,应用关闭时自动关闭:

@Configuration
public class ExecutorConfig {
    @Bean(destroyMethod = "shutdown")
    public ExecutorService customExecutorService() {
        return Executors.newFixedThreadPool(4);
    }
}

然后在你的控制器/服务类中注入这个Bean:

@Autowired
private ExecutorService customExecutorService;

// 使用时直接传入这个Bean即可,无需手动关闭
CompletableFuture<IStoryDetail> iStoryDetailCompletableFuture = CompletableFuture.supplyAsync(() -> storyRepository.getStoryDetails(id), customExecutorService);
// ... 其他任务代码

这样Spring会在应用停止时自动调用shutdown(),既避免了手动关闭的时机问题,也实现了线程池的复用,提升性能。


为什么之前的方式会阻塞?

当你只等待原始supplyAsync任务完成就调用shutdown()时,thenApply的回调可能还在ExecutorService的线程中运行。shutdown()会让线程池停止接受新任务,但会等待所有已提交的任务完成。如果此时后续请求又尝试使用同一个线程池(如果是复用的话),或者你的控制器线程在等待线程池关闭时被阻塞,就会导致后续请求无法处理。

内容的提问来源于stack exchange,提问作者javaworld

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.06 23:12:47