Java并发编程(八)——异步编程之CompletableFuture

目录

1、CompletableFuture

1.1 常用方法

1.1.1 supplyAsync()

1.1.2 runAsync()

1.1.3 get()、join()、allOf()

1.1.4 thenApply()

1.1.5 thenAccept()

1.1.6 thenCompose()

1.2 常用功能

1.2.1 非阻塞性

1.2.2 组合性和链式调用

1.2.3 错误处理

1.2.4 超时管理

1.2.5 完成通知

1.3 适用场景

1.4 异步编程

1.5 总结


异步编程是一种编程模型,用于处理并发、并行和非阻塞的任务。它通过将长时间执行的操作放在后台运行,并在完成后通知主线程,以提高程序的性能和响应性

那为什么需要使用异步?

🌰场景:假设某业务需要查询10w条数据

同步查询

如果使用SQL语句select * from t_table 同步查询:

  • 一次性查询10w条数据
  • 那接口就需等待数据库查询结束后响应,这可能导致长时间的阻塞

用户就需要等待很长时间的时间,那用户体验肯定不理想呀,为了解决同步阻塞的问题,就需要采用异步编程

异步查询

  • 将10w条数据分成若干小批量数据,如100批,每个批次1000条
  • 并发执行这100批数据
  • 等待所有查询完成再响应接口,那这样就减少每次数据查询的数据量,提高并发度

接下来就需要先了解异步编程非阻塞操作的类了,再实现异步查询

1、CompletableFuture

CompletableFuture 是 Java 8 引入的一个高级异步编程 API,提供了一种更直观、更强大的方式来编写异步和并行代码,使得复杂的并发任务变得更加易于实现和维护

📌特点

  • 实现了 Future 接口和 CompletionStage 接口
public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {
    volatile Object result;       // Either the result or boxed AltResult
    volatile Completion stack;    // Top of Treiber stack of dependent actions
    ……
}
  • 支持组合和流式的异步操作
  • 可以通过回调或链式调用来处理结果或异常
  • 提供了静态工厂方法来创建异步任务
  • 支持阻塞和非阻塞的方式获取结果

1.1 常用方法

常用方法分为以下几类:

  • 创建异步任务:supplyAsync()、runAsync()
  • 获取任务结果:get()、getNow()、join()、complete()、allOf()、……
  • 异步回调处理:thenApply、theAccept、thenCompose……
  • 异常处理:exceptionally、handle()、completeExceptionally()……
  • 超时管理:orTimeout

1.1.1 supplyAsync()

🥀:功能:创建带有返回值的异步任务

它有如下两个方法,一个是使用默认线程池(ForkJoinPool.commonPool())的方法,一个是带有自定义线程池的重载方法,不建议使用默认的线程池,可以看这篇文章:Java并发编程(六)—线程池的使用-CSDN博客,源码:

// 带返回值异步请求,默认线程池
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)

// 带返回值的异步请求,可以自定义线程池
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)

使用方法:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello CompletableFuture!");
  

1.1.2 runAsync()

🥀功能:创建不带有返回值的异步任务

源码:

// 不带返回值的异步请求,可以自定义线程池
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)

示例:

CompletableFuture<Void> future = CompletableFuture.runAsync(() -> System.out.println("Hello CompletableFuture!"));

1.1.3 get()、join()、allOf()

get()

🥀功能:阻塞当前线程直到任务完成,并返回计算结果

源码:

public V get() throws InterruptedException, ExecutionException {
      int s;
      if ((s = mode) <= 0)
          s = awaitDone(0L, null);
      if (s >= COMPLETING)
          return reportGet(result);
      throw new CancellationException();
}

示例:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello CompletableFuture!");
System.out.println(future.get());// 阻塞直到完成

输出结果:

Hello CompletableFuture!

 join()

🥀功能:阻塞当前线程直到任务 完成,并返回计算结果。与 get() 类似,但抛出的是未检查异常

源码:

public V join() {
      if (!isDone())
          Thread.interrupted(); // fail fast if already interrupted
      try {
          return get();
      } catch (CancellationException x) {
          throw new CancellationException(x.getMessage());
      } catch (ExecutionException x) {
          throw new CompletionException(x.getCause());
      } catch (InterruptedException x) {
          Thread.currentThread().interrupt(); // preserve interrupt status
          throw new CompletionException(x);
      }
}

示例:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello CompletableFuture!");
System.out.println(future.join());

 输出结果:

Hello CompletableFuture!

allOf()

🥀:功能:等待所有异步任务完成

源码:

public static CompletableFuture<Void> allOf(CompletableFuture<?>... others) {
      if (others == null || others.length == 0)
          return completedFuture(null);
      return new AllOf<>(others).fork();
}
  

示例:

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

其他方法getNow()、complete()、completeExceptionally()先做了解,后续使用时会详细讲解

// 如果完成则返回结果,否则就抛出具体的异常
public T get() throws InterruptedException, ExecutionException 

// 最大时间等待返回结果,否则就抛出具体异常
public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException

// 完成时返回结果值,否则抛出unchecked异常。为了更好地符合通用函数形式的使用,如果完成此 CompletableFuture所涉及的计算引发异常,则此方法将引发unchecked异常并将底层异常作为其原因
public T join()

// 如果完成则返回结果值(或抛出任何遇到的异常),否则返回给定的 valueIfAbsent。
public T getNow(T valueIfAbsent)

// 如果任务没有完成,返回的值设置为给定值
public boolean complete(T value)

// 如果任务没有完成,就抛出给定异常
public boolean completeExceptionally(Throwable ex) 

1.1.4 thenApply()

🥀:功能:某个任务执行完成后对结果进行处理,返回一个新的结果

源码:

public <V> CompletionStage<V> thenApply(Function<? super T, ? extends V> fn) {
      return uniApply(new CompletedStage<>(this), fn);
}
  

示例:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello")
                                                      .thenApply(s -> s + " CompletableFuture!");
  
System.out.println(future.get()); //输出结果

这段代码的含义是:

  1. 通过supplyAsync()方法指定了一个异步计算的任务,该任务返回一个字符串"Hello"
  2. 然后调用thenApply()方法将一个Function函数应用到该结果字符串上,将其变为"Hello World"
  3. 使用get()输出结果
Hello CompletableFuture!

总之,thenApply()方法允许我们对CompletableFuture的结果进行转换和处理,并产生一个新的CompletableFuture

1.1.5 thenAccept()

🥀:功能:某个任务执行完成后对结果进行处理,但不返回任何结果

源码:

public CompletionStage<Void> thenAccept(Consumer<? super T> action) {
      return uniWhenComplete(this, new ActionOnly(action));
}

示例:

CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> "Hello")
                                                  .thenAccept(result -> { System.out.println("Received result: " + result); });

这段代码的含义是:

  1. 通过supplyAsync()方法指定了一个异步计算的任务,该任务返回一个字符串"Hello"。
  2. 然后调用thenAccept()方法,传入一个Consumer函数式接口,当异步计算完成后,该Consumer会被调用,并输出结果
Received result: Hello

⚠️注意:thenAccept()方法是一个异步方法,它并不会阻塞当前线程。如果需要等待异步操作完成,可以使用join()方法

1.1.6 thenCompose()

🥀:功能:某个任务执行完成后,接收上一个任务的结果,‌并返回一个新的CompletableFuture对象

源码:

  public <V> CompletionStage<V> thenCompose(Function<? super T, ? extends CompletionStage<V>> fn) {
      return uniCompose(this, fn);
  }
  

示例:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello")
                                                      .thenCompose(s -> CompletableFuture.supplyAsync(() -> s + " CompletableFuture!"));

这段代码的含义:

  • 调用supplyAsync方法进行异步执行,返回一个字符串"Hello"
  • 然后使用thenCompose方法对这个CompletableFuture对象进行处理:它接受一个函数参数s -> CompletableFuture.supplyAsync(() -> s + " CompletableFuture!")
  • 该函数将前一个CompletableFuture的结果作为输入,并返回一个新的CompletableFuture对象
  • 最终返回的CompletableFuture对象的结果是
Hello CompletableFuture!

thenCompose() 方法的一个重要特性是它可以嵌套使用,以便在更复杂的场景中连接多个 CompletableFuture

1.2 常用功

CompletableFuture  专注于异步任务的结果,并提供丰富的API用于组合和错误处理,它负责:

  • 并行处理:可以将多个独立的任务并行执行,然后合并结果
  • 异步回调:可以在任务完成后执行回调函数,而不阻塞主线程
  • 异常处理:在异步操作中更方便地处理异常情况

下来,结合Future来了解CompletableFuture的功能

1.2.1 非阻塞性

问题:Future 的 get() 方法会阻塞调用线程,直到计算完成。
🍃解决方案:CompletableFuture 提供了多种非阻塞的方法来处理结果

先了解 Future 接口以及它的一些局限性,并且分析如何使用 CompletableFuture 类来解决这些问题

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;
}

其中Future 接口只提供了 get() 方法来获取计算结果,但如果计算时间过长,我们的线程就会一直堵塞

为了更好地理解,让我们看一些示例代码:

ExecutorService提交一个线程睡眠的任务threadSleep(),然后通过 future.get() 在控制台上打印结果值,但是由于threadSleep()中让线程沉睡了10年,因此需等待10年以后控制台才会打印值,而且 Future 也没有任何方法可以手动完成任务

public class FutureExample {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        Future<String> future = executor.submit(() -> threadSleep());
        System.out.println("The result is: " + future.get());
    }

    private static String threadSleep() throws InterruptedException {
        TimeUnit.DAYS.sleep(365 * 10);
        return "finishSleep";
    }
}

CompletableFuture如何解决这个问题呢?

public class CompletableFutureExample {

    public static void main(String[] args) {
        CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> threadSleep());
        completableFuture.complete("Completed");
        System.out.println("result: " + completableFuture.get());
        System.out.println("completableFuture done ? " + completableFuture.isDone());
    }

    private static String threadSleep(){
        try {
            TimeUnit.DAYS.sleep(365 * 10);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return "finishSleep";
    }
}

控制台输出;

result: Completed
completableFuture done ? true

可见任务提前完成了,上述代码的步骤是:

  1. CompletableFuture.supplyAsync() 异步调用 threadSleep()
  2.  在supplyAsync () 调用之后立即执行 completableFuture.complete("Completed") ,意味着即使 threadSleep() 还没有开始执行,CompletableFuture 已经被标记为完成
  3. 调用 isDone() 方法,并打印其结果值

其中使用了complete(T value)

public boolean complete(T value) {
        boolean triggered = completeValue(value);
        postComplete();
        return triggered;
}

通过源码可以知道:complete(T value) 方法是用于手动完成一个 CompletableFuture 任务(即使任务尚未执行或未完成)并且返回 value

可以看出CompletableFuture使用complete(T value)方法手动结束 任务,从而客服了 Futrue 无法手动结束任务的限制

1.2.2 组合性和链式调用

❓问题:Future 不支持直接的组合操作,难以将多个 Future 的结果组合在一起。
🍃解决方案:CompletableFuture 支持链式调用和组合操作。

public class ChainingTasksExample {
    public static void main(String[] args) {
        CompletableFuture.supplyAsync(() -> "Task 1")
            .thenApply(result -> result + " + Task 2")
            .thenApply(result -> result + " + Task 3")
            .thenAccept(System.out::println);
    }
}

这段代码的含义:创建一个任务返回Task 1,作为thenApply()的输入并返回新的任务Task2、Task3,使用thenAccept输出结果

Task 1 + Task 2 + Task 3

也就是CompletableFuture如何通过链式调用和结果转换来组合多个异步任务。每个thenApply方法都会在上一个任务完成后异步执行,并将结果传递给下一个任务。最后,thenAccept方法用于消费最终的结果

1.2.3 错误处理

❓问题:Future 在处理异常时不够灵活
🍃解决方案:CompletableFuture 提供了多种方法来处理异常

public class ErrorHandlingExample {
    public static void main(String[] args) {
        CompletableFuture.supplyAsync(() -> {
            if (true) throw new RuntimeException("Something went wrong!");
            return "Success";
        }).exceptionally(ex -> {
            System.out.println("Error: " + ex.getMessage());
            return "Error occurred";
        }).thenAccept(System.out::println);
    }
}

这段代码的含义:在任务处理中发生异常,exceptionally处理了异常,并使用thenAccept输出结果

Error occurred

CompletableFuture提供了多种方法来处理异步任务执行过程中发生的异常。您可以使用exceptionallyhandle和等方法whenComplete来妥善处理错误

1.2.4 超时管理

❓问题:Future 没有提供超时管理机制。
🍃解决方案:CompletableFuture 支持完成通知

public class TimeoutManagementExample {
    public static void main(String[] args) {
        CompletableFuture.supplyAsync(() -> {
                    try {
                        TimeUnit.SECONDS.sleep(5);
                    } catch (InterruptedException e) {
                        throw new IllegalStateException(e);
                    }
                    return "Result after delay";
                }).orTimeout(2, TimeUnit.SECONDS)
                .exceptionally(ex -> "Timeout occurred")
                .thenAccept(System.out::println);
    }
}

这段代码的含义:任务完成需要5s,若任务超过2s,抛出异常,exceptionally接受异常,返回异常结果,thenAccpet输出结果:

//异常结果
Timeout occurred

//正常结果
Result after delay

CompletableFuture提供 orTimeout有效地处理超时

1.2.5 完成通知

❓问题:Future 没有提供完成通知机制。
🍃解决方案:CompletableFuture 支持完成通知

public class CompletionNotificationExample {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            return "Data loaded";
        }).thenRun(() -> System.out.println("Data is ready"));

        // 主线程不会被阻塞
        System.out.println("Main thread continues...");
    }
}

这段代码的含义:在任务完成后,theRun()打印 "Data is ready",且不阻塞主线程,输出结果:

Main thread continues...
Data is ready

这里theRunthenAccept有一点类似,用于在异步任务完成后执行后续操作的方法,但它们之间有一些关键的区别:

thenRun

  • 不接受任何参数:thenRun 方法用于执行一个不依赖于异步任务结果的操作。
  • 返回类型:thenRun 返回一个 CompletableFuture<Void>,表示后续的操作没有返回值。
  • 用途:通常用于执行一些副作用操作,如日志记录、资源释放等。

thenAccept

  •  接受一个参数:thenAccept 方法接受一个 Consumer<T> 类型的参数,其中 T 是异步任务的返回类型。这意味着它可以访问异步任务的结果。
  • 返回类型:同样返回一个 CompletableFuture<Void>,表示后续的操作没有返回值。
  • 用途:通常用于处理异步任务的结果,例如打印结果、更新状态等

简而言之:就是theRun不接受参数,thenAccept而接受上一个任务的结果作为入参,直接做数据处理,不返回结果

1.3 适用场景

CompletableFuture 主要在处理异步编程和并发任务时使用,还有其他非阻塞 I/O、事件驱动处理、异常处理、任务组合等场景中使用,以下是一些适合使用 CompletableFuture 的典型场景:

批量处理任务

当需要并行处理多个独立任务时,使用 CompletableFuture 可以有效利用多核 CPU,提高计算效率。

异步数据加载

在进行 I/O 操作时,例如文件读取、数据库访问或网络请求,使用 CompletableFuture 可以使这些操作异步执行,从而避免阻塞主线程,提高应用程序的响应性。

流水线处理

当有一系列依赖的操作需要按顺序执行时,CompletableFuture 可以使这些操作异步执行,形成处理流水线,从而提高处理效率。

事件驱动的异步处理

在事件驱动的系统中,例如 GUI 应用或服务器请求处理,CompletableFuture 可以在事件发生时异步处理任务。

异常处理和回退机制

CompletableFuture 提供了灵活的异常处理机制,可以在异步任务发生异常时执行回退操作或提供默认值。

并发任务的组合

CompletableFuture 可以组合多个并发任务的结果,例如使用 allOf 和 anyOf 方法。

1.4 异步编程

好了,对于CompletableFuture已经了解的部分了,现在来处理上面提到的🌰

对于 10 w条数据的异步查询,可以采用CompletableFuture来实现:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class AsyncQueryExample {

    public static void main(String[] args) throws Exception {
        // 创建一个固定大小的线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(10);

        // 原始列表
        List<Integer> originalList = IntStream.rangeClosed(1, 100000).boxed().collect(Collectors.toList());

        // 分割列表
        List<List<Integer>> subLists = split(originalList, 1000);  // 每批 1000 条记录

        // 创建 CompletableFuture 列表
        List<CompletableFuture<List<String>>> futures = subLists.stream()
                .map(subList -> CompletableFuture.supplyAsync(() -> getStrings(subList), threadPool))
                .collect(Collectors.toList());

        // 使用 CompletableFuture.allOf 并发执行所有查询
        CompletableFuture<Void> allFutures = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));

        // 等待所有查询完成
        allFutures.join();

        // 合并所有 CompletableFuture 的结果
        List<String> combinedResults = futures.stream()
                .map(CompletableFuture::join)
                .flatMap(List::stream)
                .collect(Collectors.toList());

        // 关闭线程池
        threadPool.shutdown();

        // 输出结果
        System.out.println("Combined Results: " + combinedResults);
    }

    // 分割列表的方法
    public static <T> List<List<T>> split(List<T> list, int size) {
        List<List<T>> result = new ArrayList<>();
        for (int i = 0; i < list.size(); i += size) {
            result.add(new ArrayList<>(list.subList(i, Math.min(i + size, list.size()))));
        }
        return result;
    }

    // 模拟查询方法
    public static List<String> getStrings(List<Integer> integers) {
        return integers.stream()
                .map(integer -> "Result for " + integer)
                .collect(Collectors.toList());
    }
}

代码步骤如下:

  • 数据分割

将 10 万条数据分割成若干个小批量,每个批量包含一定数量的数据(例如 1000 条)。
这样可以减少每次查询的数据量,提高并发度。

  • 创建线程池【建议使用自定义线程池,newFixedThreadPool仅演示案例使用】

创建一个固定大小的线程池,用于执行并发查询任务。
线程池的大小可以根据实际的 CPU 核心数和预期的并发度来调整。

  • 创建 CompletableFuture

对于每个分割好的小批量数据,创建一个 CompletableFuture。
使用 supplyAsync 方法提交任务到线程池中执行。

  • 并发执行查询

使用 CompletableFuture.allOf 方法并发执行所有查询。
这样可以确保所有的查询任务同时开始执行。

  • 等待所有查询完成

使用 CompletableFuture.join 或 CompletableFuture.allOf(...).join() 方法等待所有查询完成。
这一步是必要的,以确保所有查询的结果都准备好后才进行下一步。

  • 合并结果

收集所有 CompletableFuture 的结果,并将它们合并成最终的结果集。
这可以通过流式处理来实现。

  • 异常处理

处理可能出现的任何异常情况,例如查询失败或网络错误等。
可以使用 handle 或 exceptionally 方法来处理异常

  • 关闭线程池
  • 输出结果

从上述可以看出:

数据量与异步查询:当数据量较大时,异步查询可以显著提高性能。
并发执行:通过并发执行多个小批量查询,可以提高整体处理速度。
等待所有查询完成:使用 CompletableFuture.allOf join 方法等待所有查询完成,并合并结果

这种方式可以有效地利用异步查询的优势,即使每个查询都是异步执行的,也可以确保所有查询并发执行,从而提高整体性能

1.5 总结

CompletableFuture是一种用于处理异步任务和并发编程的强大工具类,支持异步任务的创建、组合、变换和错误处理,提供了丰富的 API 来简化并发编程,提供了丰富的链式调用方式,支持非阻塞的异步编程模式

  • 14
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值