使用CompletableFuture时ExecutorService无法正常关闭的问题排查
首先,咱们来拆解下你遇到的问题:用自定义ExecutorService执行CompletableFuture任务后,调用shutdown()无法正常关闭,还导致后续请求阻塞,但移除关闭代码就正常。核心原因有两个:
你只等待了原始异步任务完成,没等待回调任务:
CompletableFuture.allOf()只等待了三个supplyAsync的任务,但你后续的thenApply回调是依赖这些任务的新异步任务,allOf()并没有等待这些回调执行完毕就触发了shutdown()。这时候ExecutorService可能还有回调任务在运行,shutdown()会等待这些任务完成,但如果你的控制器没等所有任务结束就返回,就会导致线程池的关闭逻辑和后续请求产生冲突。如果
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

