MST

星途 面试题库

面试题:Java中CompletableFuture的supplyAsync与runAsync在异步任务异常处理上的差异

在使用CompletableFuture的supplyAsync和runAsync方法创建异步任务时,分别说明如何进行异常处理,包括未捕获异常时的默认行为以及如何自定义异常处理逻辑,同时举例说明在复杂业务场景下两者异常处理方式的选择依据。
48.5万 热度难度
编程语言Java

知识考点

AI 面试

面试题答案

一键面试

supplyAsync 异常处理

  1. 未捕获异常默认行为
    • supplyAsync 方法返回 CompletableFuture<U>,如果异步任务执行过程中抛出未捕获的异常,该异常会被包装在 CompletableFuture 中。当调用 get() 等阻塞方法获取结果时,会抛出 ExecutionException,其内部包含原始异常。例如:
    CompletableFuture.supplyAsync(() -> {
        throw new RuntimeException("Test exception");
    }).thenApply(result -> {
        // 这部分不会执行
        return result;
    }).exceptionally(ex -> {
        // 这部分会处理异常
        System.out.println("Caught exception: " + ex.getMessage());
        return null;
    });
    
    当执行 get() 方法时,如果没有处理异常,会得到类似如下异常栈:
    java.util.concurrent.ExecutionException: java.lang.RuntimeException: Test exception
        at java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:357)
        at java.util.concurrent.CompletableFuture.get(CompletableFuture.java:1908)
        ...
    Caused by: java.lang.RuntimeException: Test exception
        ...
    
  2. 自定义异常处理逻辑
    • 使用 exceptionally 方法:可以在 CompletableFuture 链中使用 exceptionally 方法来处理异常。它接收一个 Function<Throwable, U> 类型的参数,当异步任务抛出异常时,会调用这个 Function,返回值会作为整个 CompletableFuture 的结果。例如:
    CompletableFuture.supplyAsync(() -> {
        if (Math.random() < 0.5) {
            throw new RuntimeException("Random exception");
        }
        return "Success result";
    }).exceptionally(ex -> {
        System.out.println("Exception handled: " + ex.getMessage());
        return "Default value";
    }).thenAccept(System.out::println);
    
    • 使用 whenComplete 方法whenComplete 方法接收一个 BiConsumer<? super U,? super Throwable> 类型的参数,无论异步任务正常完成还是抛出异常,都会调用这个 BiConsumer。可以在其中判断是否有异常并进行处理。例如:
    CompletableFuture.supplyAsync(() -> {
        throw new RuntimeException("Test exception");
    }).whenComplete((result, ex) -> {
        if (ex != null) {
            System.out.println("Caught exception in whenComplete: " + ex.getMessage());
        } else {
            System.out.println("Result: " + result);
        }
    });
    

runAsync 异常处理

  1. 未捕获异常默认行为
    • runAsync 方法返回 CompletableFuture<Void>,如果异步任务执行过程中抛出未捕获的异常,默认情况下该异常会被忽略。例如:
    CompletableFuture.runAsync(() -> {
        throw new RuntimeException("Test exception");
    });
    
    这段代码不会打印任何异常信息(在没有设置全局异常处理器等特殊情况下)。
  2. 自定义异常处理逻辑
    • 使用 exceptionally 方法:与 supplyAsync 类似,可以使用 exceptionally 方法处理异常。不过因为 runAsync 返回 CompletableFuture<Void>,所以 exceptionally 方法的返回值也应该是 Void。例如:
    CompletableFuture.runAsync(() -> {
        if (Math.random() < 0.5) {
            throw new RuntimeException("Random exception");
        }
        System.out.println("Task completed successfully");
    }).exceptionally(ex -> {
        System.out.println("Exception handled: " + ex.getMessage());
        return null;
    });
    
    • 使用 whenComplete 方法:同样可以使用 whenComplete 方法处理异常,在 BiConsumer 中判断是否有异常。例如:
    CompletableFuture.runAsync(() -> {
        throw new RuntimeException("Test exception");
    }).whenComplete((v, ex) -> {
        if (ex != null) {
            System.out.println("Caught exception in whenComplete: " + ex.getMessage());
        } else {
            System.out.println("Task completed successfully");
        }
    });
    

复杂业务场景下两者异常处理方式的选择依据

  1. 根据返回值需求选择
    • 如果业务场景需要返回结果,使用 supplyAsync。例如在一个电商系统中,需要异步查询商品库存并返回库存数量,就用 supplyAsync。在异常处理上,使用 exceptionally 可以返回一个默认库存值等处理方式,以保证整个业务流程不会因为异常而中断后续依赖库存值的操作。
    • 如果业务场景只是执行一个异步操作,不需要返回结果,如异步记录日志、发送通知等,使用 runAsync。在异常处理上,对于日志记录操作,如果失败,可以在 exceptionallywhenComplete 中记录异常日志,防止因为日志记录失败影响主业务流程。
  2. 根据异常处理复杂度选择
    • 如果异常处理逻辑比较简单,只是记录异常信息或者返回一个默认值,exceptionally 方法简洁明了。例如在一个简单的异步文件读取操作(使用 supplyAsync 返回文件内容)中,异常时返回空字符串。
    • 如果异常处理逻辑复杂,需要根据不同类型的异常做不同处理,可能涉及到资源清理、通知其他系统等操作,whenComplete 方法更合适。例如在一个异步数据库操作(使用 runAsync)中,不同的数据库异常需要不同的处理方式,在 whenComplete 中可以更灵活地编写处理逻辑。