MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java 中 CompletableFuture 与 Future 对比

2023-09-032.9k 阅读

Java 中的 Future 接口

  1. Future 接口概述

    • Future接口是 Java 并发包中用于异步计算结果的基础接口。它提供了一种机制来获取异步任务的执行结果、检查任务是否完成、取消任务等操作。在 Java 5 引入java.util.concurrent包时,Future接口就成为了异步编程的重要组成部分。
    • 其定义如下:
    public interface Future<V> {
        boolean cancel(boolean mayInterruptIfRunning);
        boolean isCancelled();
        boolean isDone();
        V get() throws InterruptedException, ExecutionException;
        V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
    }
    
  2. Future 接口的方法解析

    • cancel(boolean mayInterruptIfRunning):尝试取消任务的执行。如果任务已经完成、已经被取消或者由于某些原因无法取消,该方法将返回false。如果任务还没有开始执行,任务将被取消且不再执行。如果任务已经开始执行,并且mayInterruptIfRunning参数为true,那么正在执行任务的线程可能会被中断(如果线程支持中断)。
    • isCancelled():判断任务是否在正常完成之前被取消。如果任务在完成之前被取消,返回true,否则返回false
    • isDone():判断任务是否已经完成。任务完成包括正常结束、异常结束或被取消等情况。如果任务已经完成,返回true,否则返回false
    • get():等待任务完成,并返回任务的结果。如果任务在等待过程中被中断,将抛出InterruptedException;如果任务执行过程中抛出异常,将抛出ExecutionException,该异常的getCause()方法可以获取实际的异常原因。
    • get(long timeout, TimeUnit unit):在指定的时间内等待任务完成,并返回任务的结果。如果在指定时间内任务没有完成,将抛出TimeoutException。同时,如果在等待过程中被中断或任务执行抛出异常,同样会抛出相应的InterruptedExceptionExecutionException
  3. 使用 Future 的示例

    • 下面通过一个简单的示例来展示Future的使用。假设我们有一个耗时的计算任务,计算从 1 到指定数字的累加和。
    import java.util.concurrent.*;
    
    public class FutureExample {
        public static void main(String[] args) {
            ExecutorService executor = Executors.newSingleThreadExecutor();
            Future<Integer> future = executor.submit(() -> {
                int sum = 0;
                for (int i = 1; i <= 1000000; i++) {
                    sum += i;
                }
                return sum;
            });
    
            try {
                System.out.println("等待任务完成...");
                Integer result = future.get();
                System.out.println("计算结果: " + result);
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            } finally {
                executor.shutdown();
            }
        }
    }
    
    • 在这个示例中,我们通过ExecutorServicesubmit方法提交一个异步任务,该任务返回一个Future对象。然后通过future.get()方法等待任务完成并获取结果。如果任务执行过程中出现异常,get方法会抛出相应的异常。
  4. Future 的局限性

    • 阻塞获取结果Futureget方法是阻塞的,这意味着调用线程会一直等待直到任务完成,这在一些情况下会降低程序的并发性能。例如,如果有多个异步任务,并且需要按顺序获取结果,使用Future会导致线程在等待每个任务结果时处于阻塞状态,无法充分利用多核处理器的优势。
    • 缺乏链式调用和组合操作Future接口没有提供直接的链式调用和组合操作的方法。如果一个异步任务的结果需要作为另一个异步任务的输入,使用Future实现起来会比较繁琐,需要手动管理任务的依赖关系。
    • 异常处理不够灵活Future的异常处理是通过在get方法中抛出ExecutionException来实现的,这意味着调用者必须在获取结果的地方捕获异常,无法像普通方法调用那样在不同的位置处理异常,使得异常处理的灵活性较差。

Java 中的 CompletableFuture

  1. CompletableFuture 概述
    • CompletableFuture是 Java 8 引入的类,它实现了Future接口和CompletionStage接口。CompletableFuture提供了更强大的异步编程能力,克服了Future接口的一些局限性。它允许异步任务以链式调用和组合的方式进行处理,并且提供了更灵活的异常处理机制。
    • CompletableFuture可以由其他线程完成,也可以由调用者手动完成,这使得它在异步编程场景中更加灵活。同时,它支持多种静态方法来创建不同类型的CompletableFuture实例,例如CompletableFuture.supplyAsync用于创建有返回值的异步任务,CompletableFuture.runAsync用于创建无返回值的异步任务。
  2. CompletableFuture 的创建方式
    • CompletableFuture.runAsync(Runnable runnable):创建一个异步执行的CompletableFuture,该任务没有返回值。任务会使用ForkJoinPool.commonPool()作为默认的执行器来执行。
    CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> {
        // 执行异步任务
        System.out.println("异步任务执行中...");
    });
    
    • CompletableFuture.runAsync(Runnable runnable, Executor executor):与上一个方法类似,但可以指定自定义的Executor来执行任务。
    ExecutorService executor = Executors.newFixedThreadPool(10);
    CompletableFuture<Void> future2 = CompletableFuture.runAsync(() -> {
        // 执行异步任务
        System.out.println("异步任务执行中...");
    }, executor);
    
    • CompletableFuture.supplyAsync(Supplier<U> supplier):创建一个有返回值的异步任务,任务的返回值类型为U。同样使用ForkJoinPool.commonPool()作为默认执行器。
    CompletableFuture<Integer> future3 = CompletableFuture.supplyAsync(() -> {
        int result = 0;
        for (int i = 1; i <= 100; i++) {
            result += i;
        }
        return result;
    });
    
    • CompletableFuture.supplyAsync(Supplier<U> supplier, Executor executor):指定自定义Executor来执行有返回值的异步任务。
    CompletableFuture<Integer> future4 = CompletableFuture.supplyAsync(() -> {
        int result = 0;
        for (int i = 1; i <= 100; i++) {
            result += i;
        }
        return result;
    }, executor);
    
  3. CompletableFuture 的链式调用和组合操作
    • thenApply(Function<? super U,? extends V> fn):当CompletableFuture完成时,将其结果作为参数传递给fn函数,并返回一个新的CompletableFuture,其结果为fn函数的返回值。
    CompletableFuture.supplyAsync(() -> 10)
                     .thenApply(result -> result * 2)
                     .thenApply(finalResult -> "最终结果: " + finalResult)
                     .thenAccept(System.out::println);
    
    • thenAccept(Consumer<? super U> action):当CompletableFuture完成时,将其结果作为参数传递给action消费者,返回一个新的CompletableFuture,但该CompletableFuture的结果为null
    CompletableFuture.supplyAsync(() -> 10)
                     .thenAccept(result -> System.out.println("结果: " + result));
    
    • thenRun(Runnable action):当CompletableFuture完成时,执行action,返回一个新的CompletableFuture,其结果为null。这里action不接受前一个CompletableFuture的结果作为参数。
    CompletableFuture.supplyAsync(() -> 10)
                     .thenRun(() -> System.out.println("任务完成"));
    
    • thenCombine(CompletableFuture<? extends U> other, BiFunction<? super T,? super U,? extends V> fn):将当前CompletableFuture和另一个CompletableFuture的结果作为参数传递给fn函数,并返回一个新的CompletableFuture,其结果为fn函数的返回值。
    CompletableFuture<Integer> futureA = CompletableFuture.supplyAsync(() -> 5);
    CompletableFuture<Integer> futureB = CompletableFuture.supplyAsync(() -> 10);
    CompletableFuture<Integer> combinedFuture = futureA.thenCombine(futureB, (a, b) -> a + b);
    combinedFuture.thenAccept(System.out::println);
    
    • allOf(CompletableFuture<?>... cfs):返回一个新的CompletableFuture,当所有给定的CompletableFuture都完成时,该新的CompletableFuture才完成。其结果为null
    CompletableFuture<Void> allFuture = CompletableFuture.allOf(
        CompletableFuture.runAsync(() -> System.out.println("任务1")),
        CompletableFuture.runAsync(() -> System.out.println("任务2"))
    );
    allFuture.thenRun(() -> System.out.println("所有任务完成")).join();
    
    • anyOf(CompletableFuture<?>... cfs):返回一个新的CompletableFuture,当任何一个给定的CompletableFuture完成时,该新的CompletableFuture就完成,其结果为第一个完成的CompletableFuture的结果。
    CompletableFuture<String> futureX = CompletableFuture.supplyAsync(() -> {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "任务X完成";
    });
    CompletableFuture<String> futureY = CompletableFuture.supplyAsync(() -> {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "任务Y完成";
    });
    CompletableFuture<Object> anyFuture = CompletableFuture.anyOf(futureX, futureY);
    anyFuture.thenAccept(System.out::println).join();
    
  4. CompletableFuture 的异常处理
    • exceptionally(Function<Throwable,? extends T> fn):当CompletableFuture执行过程中出现异常时,会调用fn函数,fn函数的参数为异常对象,返回值将作为新的CompletableFuture的结果。
    CompletableFuture<Integer> futureWithException = CompletableFuture.supplyAsync(() -> {
        if (Math.random() < 0.5) {
            throw new RuntimeException("模拟异常");
        }
        return 10;
    });
    futureWithException.exceptionally(ex -> {
        System.out.println("捕获到异常: " + ex.getMessage());
        return -1;
    }).thenAccept(System.out::println);
    
    • handle(BiFunction<? super T, Throwable,? extends U> fn):无论CompletableFuture正常完成还是出现异常,都会调用fn函数。fn函数的第一个参数为正常完成时的结果(如果出现异常则为null),第二个参数为异常对象(如果正常完成则为null),返回值将作为新的CompletableFuture的结果。
    CompletableFuture<Integer> futureForHandle = CompletableFuture.supplyAsync(() -> {
        if (Math.random() < 0.5) {
            throw new RuntimeException("模拟异常");
        }
        return 10;
    });
    futureForHandle.handle((result, ex) -> {
        if (ex!= null) {
            System.out.println("捕获到异常: " + ex.getMessage());
            return -1;
        } else {
            return result * 2;
        }
    }).thenAccept(System.out::println);
    

CompletableFuture 与 Future 的对比

  1. 异步任务的组合与链式调用
    • FutureFuture接口缺乏直接的组合和链式调用能力。如果需要将多个异步任务进行组合,例如一个任务的结果作为另一个任务的输入,使用Future需要手动管理任务的依赖关系。通常的做法是在获取第一个任务的结果后,再提交第二个任务,这会导致代码变得复杂且难以维护。
    • CompletableFutureCompletableFuture提供了丰富的方法来支持异步任务的组合和链式调用。通过thenApplythenCombine等方法,可以方便地将多个异步任务连接起来,形成一个链式的异步计算流程。这种方式使得代码更加简洁和易读,同时也能更好地利用多核处理器的并行计算能力。
  2. 阻塞与非阻塞特性
    • FutureFutureget方法是阻塞的,调用线程会一直等待任务完成才能获取结果。这在一些需要高效并发的场景下会成为性能瓶颈,因为调用线程在等待过程中无法执行其他任务。
    • CompletableFutureCompletableFuture提供了非阻塞的方式来处理异步任务的结果。通过thenApplythenAccept等方法,当任务完成时会自动触发后续的处理逻辑,而不需要调用线程阻塞等待。这使得程序可以在等待异步任务完成的同时继续执行其他任务,提高了并发性能。
  3. 异常处理机制
    • FutureFuture的异常处理相对不够灵活。异常是通过在get方法中抛出ExecutionException来传递的,这要求调用者必须在调用get方法的地方捕获异常。如果在多个地方需要获取任务结果,就需要在每个获取结果的地方重复处理异常,代码的可维护性较差。
    • CompletableFutureCompletableFuture提供了更灵活的异常处理机制。通过exceptionallyhandle等方法,可以在任务执行链的不同位置处理异常。例如,可以在任务执行的中间阶段处理异常,而不需要在获取最终结果的地方统一处理,使得异常处理更加灵活和模块化。
  4. 任务的完成方式
    • FutureFuture的任务完成通常依赖于线程池中的任务执行完成。一旦任务提交到线程池,调用者无法直接控制任务的完成状态,只能通过isDone等方法来查询任务是否完成。
    • CompletableFutureCompletableFuture不仅可以由线程池中的任务自然完成,还可以由调用者手动完成。例如,可以通过complete方法手动设置任务的结果,通过completeExceptionally方法手动设置任务的异常,这在一些需要模拟异步任务结果或者在特定条件下提前结束任务的场景中非常有用。
  5. 使用场景对比
    • Future:适用于简单的异步任务场景,当只需要获取异步任务的结果,并且对任务的组合、异常处理等要求不高时,Future是一个简单直接的选择。例如,在一些不需要复杂异步流程的工具类或者小型应用中,使用Future可以满足基本的异步计算需求。
    • CompletableFuture:适用于复杂的异步编程场景,当需要对多个异步任务进行组合、链式调用,并且需要灵活处理异常时,CompletableFuture是更好的选择。在大型的并发应用、分布式系统等场景中,CompletableFuture能够有效地提高程序的并发性能和可维护性。

综上所述,CompletableFuture在功能上比Future更加丰富和强大,它解决了Future在异步编程中的一些局限性。在实际的 Java 开发中,应根据具体的需求和场景来选择使用Future还是CompletableFuture,以实现高效、可维护的异步编程。无论是简单的异步任务还是复杂的异步计算流程,Java 的这两种异步编程工具都能为开发者提供有力的支持。