目录
异步编程是一种编程模型,用于处理并发、并行和非阻塞的任务。它通过将长时间执行的操作放在后台运行,并在完成后通知主线程,以提高程序的性能和响应性
❓那为什么需要使用异步?
🌰场景:假设某业务需要查询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()); //输出结果
这段代码的含义是:
- 通过supplyAsync()方法指定了一个异步计算的任务,该任务返回一个字符串"Hello"
- 然后调用
thenApply()
方法将一个Function
函数应用到该结果字符串上,将其变为"Hello World" - 使用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); });
这段代码的含义是:
- 通过supplyAsync()方法指定了一个异步计算的任务,该任务返回一个字符串"Hello"。
- 然后调用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
可见任务提前完成了,上述代码的步骤是:
- CompletableFuture.supplyAsync() 异步调用 threadSleep()
- 在supplyAsync () 调用之后立即执行 completableFuture.complete("Completed") ,意味着即使 threadSleep() 还没有开始执行,CompletableFuture 已经被标记为完成
- 调用 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
提供了多种方法来处理异步任务执行过程中发生的异常。您可以使用exceptionally
、handle
和等方法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
这里theRun和thenAccept有一点类似,用于在异步任务完成后执行后续操作的方法,但它们之间有一些关键的区别:
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 来简化并发编程,提供了丰富的链式调用方式,支持非阻塞的异步编程模式