使用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
相关产品推荐
相关产品推荐

