在我之前的文章IO密集型服务提升性能的三种方法中提到过,提升IO密集型应用性能有个方式就是异步编程,实现异步时一定会用到Future,使用多线程+Future我们可以让多个任务同时去执行,最后统一去获取执行结果,这样整体执行的时长就取决于最长的一个任务,比如如下代码:

public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        System.out.println(new Date());
        Future<String> future1 = executorService.submit(() -> {
            Thread.sleep(5000);
            return new Date() + ":" + "thread1 result";
        });
        Future<String> future2 = executorService.submit(() -> {
            Thread.sleep(3000);
            return new Date() + ":" + "thread2 result";
        });
        try {
            System.out.println(future1.get());
            System.out.println(future2.get());
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } catch (ExecutionException e) {
            throw new RuntimeException(e);
        }
    }

  上面代码的运行时长是5s,如果两个任务串行执行的化,运行时间就会是8s,当多个任务多线程异步执行时,最终总的执行时长取决于最长任务的执行时长。Future在大部分异步变成情况下,已经能很好的满足我们异步变成的诉求了,但当我看到CompletableFuture这个东西的时候,才发现Future还是太简单,我还是太年轻。

  CompletableFuture除了和Future一样可以获取执行结果外,它还**简化了异常、提供了手动设置结果的接口、链式操作、结果组合、回调,**接下来我们通过一些代码示例,来看下CompletableFuture这些特性如何吊打Future。

异常处理

  在使用Future时,你只能在get()的时候抓取到异常,异常处理会和结果获取的逻辑混在一起,有些时候处理起来会比较麻烦,而CompletableFuture提供了exceptionallyhandle 两个api,可以很方便的添加异常处理逻辑,后续只需要直接使用get获取结果即可,代码示例如下:

public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            // 故意抛出一个异常
            if (System.currentTimeMillis() % 2 == 0) {
                throw new RuntimeException("Exception occurred!");
            }
            return "Result";
        }, executorService);

        future = future.exceptionally(ex -> {
            // 对异常进行处理,并提供一个异常时的结果
            return "Default Value";
        });

        try {
            System.out.println(future.get()); // 输出 "Default Value"
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

  exceptionally api让任务在运行过程中,如果抓到了Exception,就可以调用你给定的逻辑来处理异常。 与exceptionally就是handle方法,达到的效果一致,但使用略有不同,可以看出handle这个api是在任务执行完成后,讲exception和执行结果共同处理一次。

future = future.handle((result, ex) -> {
            if (ex != null) {
                return "Default Value";
            }
            return result;
        });
        try {
            System.out.println(future.get()); // 输出 "Default Value"
        } catch (Exception e) {
            e.printStackTrace();
        }

手动设置结果

  在Future中,我们无法手动地完成一个任务或者设置任务的结果,这个任务的执行结果完全取决于任务的执行情况。但在CompletableFuture,我们可以在任何时间点上手动设置结果或者标记任务为已完成。举个例子,我要通过接口获取某个数据,但很要求时效性,如果超过1s我就不要接口调用结果了,而是返回一个默认值。Future设置等待时间也可以实现类似的功能,但不是很直观, CompletableFuture就很简单直观了。

public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            try {
                // 随机sleep 0-2s
                Thread.sleep(ThreadLocalRandom.current().nextInt(2000));
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            return "Result";
        }, executorService);
        CompletableFuture<String> finalFuture = future;
        // 1s后手动完成
        executorService.submit(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 手动地完成这个 CompletableFuture,并设置计算结果
            finalFuture.complete("Default Value");
        });

        try {
            System.out.println(future.get()); // 概率性输出 "Default Value"
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

链式操作

  链式操作意味着你可以将一个CompletableFuture的结果直接传递给另一个异步操作,而无需等待第一个操作完成。这使得你可以更容易地编写复杂的并发逻辑,这种非常适合需要将多个接口结果串起来的流程,有点像langChain,我们直接看示例代码:

public static void main(String[] args) {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            // 第一个异步任务
            return "Hello";
        }).thenApplyAsync(result -> {
            // 第二个异步任务,依赖第一个任务的结果
            return result + " World";
        }).thenApplyAsync(result -> {
            // 第三个异步任务,依赖第二个任务的结果
            return result + "!";
        });
        try {
            System.out.println(future.get());
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } catch (ExecutionException e) {
            throw new RuntimeException(e);
        }
    }

  这个示例比较简单,在实际使用中,我们可以将三个任务替换成任何需要相互依赖的任务,你只需要定义每个任务的输入和输出,然后将它们连接在一起,就可以创建一个复杂的并发流程。

结果组合

  CompletableFuture提供了几个方法来组合或合并多个CompletableFuture的结果。最常用的可能是thenCombineallOf方法。

以下是thenCombine方法的使用示例:

CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
    return "Hello";
});

CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
    return "World!";
});

CompletableFuture<String> combinedFuture = future1.thenCombine(future2, (s1, s2) -> {
    return s1 + ", " + s2;
});

try {
    String result = combinedFuture.get();
    System.out.println(result); // 输出 "Hello, World!"
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}

  在上述代码中,我们首先创建了两个独立的CompletableFuture,然后使用thenCombine方法将它们的结果组合在一起。

  以下是allOf方法的使用示例:

CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
    return "Hello";
});

CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
    return "World!";
});

CompletableFuture<Void> allFutures = CompletableFuture.allOf(future1, future2);

// 当所有的 CompletableFuture 都完成时,获取结果
allFutures.thenRun(() -> {
    try {
        String result1 = future1.get();
        String result2 = future2.get();
        System.out.println(result1 + ", " + result2); // 输出 "Hello, World!"
    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    }
});

  可以看出thenCombine和allOf都实现了类似的功能,但两者的侧重点不同,thenCombine实现的是对执行结果的聚合,而allOf实现的是对执行完状态的聚合,还是需要你去显式调用get()方法去获取结果的,所以你还是得根据具体需求来选择使用thenCombine还是allOf

回调

  使用Future时,我们只能通过get()或者isDone()方法来获取到任务是否执行完成了,无法让任务执行完成后主动通知到我们,但CompletableFuture提供了一些回调方法,可以让任务执行完成后执行结果处理的任务或者执行一些通知的逻辑。 CompletableFuture提供了几个方法来设置回调,如thenApply, thenAccept, 和 thenRun

具体看代码示例:

public static void main(String[] args) {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            // 异步任务
            return "Hello, World!";
        });

        future.thenApply(result -> {
            // 当 future 完成时,这个回调会被调用。
            // result 参数是 future 的结果。
            // 这个回调的结果会被包装成一个新的 CompletableFuture。
            return result.length();
        }).thenAccept(length -> {
            // 当前面的 CompletableFuture 完成时,这个回调会被调用。
            // length 参数是前面 CompletableFuture 的结果。
            // 这个回调没有返回值。
            System.out.println(length); // 输出 "Hello, World!" 的长度
        }).thenRun(() -> {
            // 当前面的 CompletableFuture 完成时,这个回调会被调用。
            // 这个回调没有输入参数,也没有返回值。
            System.out.println("Done");
        });
    }

  thenApply、thenAccept和thenRun都是在任务执行完成后被调起,但他们定位有所不同,thenApply处理的结果可以被包装成一个新的CompletableFuture,只后可以继续链式调用。但thenAccept只能接受输入,无法提供输出,所以他的定位是任务的收尾,比如可以将结果输出。 而thenRun既没有输入也没有输出,所以它只是获取到了任务执行完的状态,任务的执行结果是获取不到的,所以它是最弱的。

总结

  总结下本文,Future 尽管有用,但在功能上还是相对简单。CompletableFuture不仅提供了获取执行结果的能力,它还增添了异常处理、手动完成、链式操作、结果组合和回调等强大功能。

  • 异常处理: 利用exceptionallyhandle方法,可以简化异常处理流程,使其更加直观和易于管理。
  • 手动设置结果: CompletableFuture允许我们在任意时间点手动设置结果,增加了灵活性。
  • 链式操作: 通过链式操作,我们可以将不同的异步操作以流水线的方式串联起来,编写复杂的并发逻辑变得更加简单。
  • 结果组合: thenCombineallOf方法能够将多个CompletableFuture的结果合并,为我们提供了更多处理异步操作结果的方式。
  • 回调: 提供了thenApply, thenAccept, 和 thenRun等方法,允许我们在任务完成后进行结果处理或其他逻辑操作。

  通过这些特性,CompletableFuture显著地提升了异步编程的能力和灵活性。无论是简化异常处理,还是实现复杂的异步逻辑,CompletableFuture都能够帮助我们写出更清晰、更高效的代码。我相信通过本文的示例代码,你可以更深入地理解CompletableFuture的强大之处,并在你的项目中充分利用它来提升性能。