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

使用Completable Future做方法JMH基准测试遇VM退出异常求助

这个问题我之前也碰到过,来给你捋捋原因和解决办法~

问题根源

JMH的forked测试VM要求所有线程都退出才能正常结束,而你遇到的核心问题是自定义Executor的线程池里留了存活的非守护线程:

  • 自定义Executor(比如ThreadPoolExecutor)默认创建的是非守护线程,这些线程即使任务完成/超时取消,只要线程池没被关闭,空闲线程会一直等着新任务,不会自动退出。
  • 你只处理了CompletableFuture的超时取消,但没处理Executor本身的生命周期,导致这些线程变成“stray threads”,卡住了VM退出。

解决方案

1. 测试结束后显式关闭Executor

用JMH的@TearDown钩子在测试结束时强制关闭线程池,确保所有线程终止。示例代码:

@State(Scope.Benchmark)
public class MyBenchmark {
    private ExecutorService customExecutor;

    @Setup(Level.Trial)
    public void initExecutor() {
        // 初始化自定义线程池
        customExecutor = Executors.newFixedThreadPool(4);
    }

    @TearDown(Level.Trial)
    public void shutdownExecutor() {
        // 停止接受新任务,立即中断所有线程
        customExecutor.shutdownNow();
        try {
            // 等待线程池完全终止,超时则强制处理
            if (!customExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
                System.err.println("线程池未及时终止,强制清理");
            }
        } catch (InterruptedException e) {
            // 恢复中断标记,不影响后续流程
            Thread.currentThread().interrupt();
        }
    }

    @Benchmark
    public void testFutureMethod() throws Exception {
        CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
            // 你的业务方法逻辑
        }, customExecutor);

        // 超时处理
        try {
            future.get(1, TimeUnit.SECONDS);
        } catch (TimeoutException | InterruptedException | ExecutionException e) {
            // 取消任务并中断执行线程
            future.cancel(true);
        }
    }
}

这里的关键是@TearDown方法里的shutdownNow(),它会直接中断线程池内的所有线程,配合awaitTermination确保线程池彻底关闭。

2. 使用守护线程构建Executor

如果不想手动关闭线程池,可以创建全部由守护线程组成的线程池。守护线程会在JVM中所有非守护线程结束后自动退出,不会卡住VM:

private ExecutorService createDaemonThreadPool() {
    ThreadFactory daemonFactory = r -> {
        Thread thread = new Thread(r);
        thread.setDaemon(true); // 标记为守护线程
        return thread;
    };
    return Executors.newFixedThreadPool(4, daemonFactory);
}

之后在@Setup里初始化这个线程池即可,测试结束后无需手动关闭,VM会自动回收这些守护线程。

3. 优化任务的中断处理

当你调用future.cancel(true)中断任务时,线程池的复用线程会保留中断标记,可能影响后续任务。建议在任务内部显式处理中断:

CompletableFuture.runAsync(() -> {
    try {
        // 业务逻辑中定期检查中断状态
        while (!Thread.currentThread().isInterrupted()) {
            // 执行任务步骤
        }
    } catch (InterruptedException e) {
        // 恢复中断标记,避免影响线程池后续复用
        Thread.currentThread().interrupt();
    }
}, customExecutor);

额外提醒

  • 务必选对JMH的@State范围:Scope.Benchmark确保每个测试进程只初始化一次线程池,避免重复创建带来的性能干扰。
  • 如果你的Executor是全局单例,一定要在测试结束后关闭,否则会影响其他测试用例。
  • shutdown()和shutdownNow()的区别:前者会等待已提交的任务完成后再关闭,后者会立即中断所有正在执行的任务,根据你的测试场景选择即可。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.19 03:37:35