一、引言:异步编程的重要性
在当今数字化时代,电商购物已成为人们生活中不可或缺的一部分。想象一下,你正在开发一个电商系统,当用户访问商品详情页时,需要同时展示多个商品的信息,包括商品的基本描述、价格、库存、用户评价等。这些信息可能来自不同的数据源,如数据库、缓存或者外部接口,获取每个商品信息的操作都可能是耗时的 I/O 操作。如果采用传统的同步编程方式,一个一个地获取商品信息,那么整个页面的加载速度将会变得非常缓慢,严重影响用户体验。
在现代 Java 开发中,类似这样需要处理大量并发任务和耗时操作的场景越来越多。而异步编程就像是一把神奇的钥匙,能够帮助我们高效地解决这些问题,显著提升程序的性能和响应性。它允许程序在执行一个耗时操作的同时,不阻塞主线程,继续执行其他任务,从而充分利用 CPU 资源,提高系统的并发处理能力。
今天,我们要深入探讨的主角 ——CompletableFuture,正是 Java 异步编程领域中的一位强大利器。它在 Java 8 中被引入,极大地简化了异步编程的复杂性,为开发者提供了更加便捷、灵活的异步操作方式,让我们能够更加优雅地处理异步任务的创建、组合、回调和异常处理等。接下来,就让我们一起揭开 CompletableFuture 的神秘面纱,领略它的强大魅力吧!
二、Java 异步编程的发展历程
在 Java 的发展长河中,异步编程的演进是一个不断突破和创新的过程,它见证了 Java 语言在应对复杂计算场景时的不断进化。
早期,Java 主要通过 Thread 类和 Runnable 接口来实现异步编程。这种方式为开发者提供了基本的多线程能力,让程序能够在多个线程中并行执行任务。例如,在开发一个简单的文件处理程序时,我们可以创建一个 Thread 类的子类,并重写其 run 方法来实现文件读取和处理的逻辑。然后,通过创建该子类的实例并调用 start 方法,就可以启动一个新的线程来执行文件处理任务,而主线程则可以继续执行其他操作。
public class FileProcessor extends Thread {private String filePath;public FileProcessor(String filePath) {this.filePath = filePath;}@Overridepublic void run() {// 模拟文件读取和处理逻辑try {Thread.sleep(2000);System.out.println("文件 " + filePath + " 处理完成");} catch (InterruptedException e) {e.printStackTrace();}}public static void main(String[] args) {FileProcessor fileProcessor = new FileProcessor("example.txt");fileProcessor.start();System.out.println("主线程继续执行其他任务");}}
不过,这种方式存在一些明显的缺点。创建和管理线程的开销较大,频繁地创建和销毁线程会消耗大量的系统资源。当需要处理多个线程之间的协作和同步时,代码会变得复杂,容易出现死锁等问题。而且,Thread 类和 Runnable 接口缺乏对异步任务结果的有效管理机制,获取任务执行结果变得很不方便。
为了解决这些问题,Java 5 引入了 Future 接口,这是 Java 异步编程发展中的一个重要里程碑。Future 接口为异步任务的管理和结果获取提供了一种标准的方式。通过 Future 接口,我们可以提交一个异步任务,并在需要的时候获取其执行结果,还能检查任务的状态,如是否完成、是否被取消等。比如,在一个电商系统中,计算商品的推荐列表可能是一个耗时的任务,我们可以使用 Future 接口来异步执行这个任务。
import java.util.concurrent.*;public class ProductRecommender {public static void main(String[] args) {ExecutorService executor = Executors.newSingleThreadExecutor();Future<List<String>> future = executor.submit(() -> {// 模拟计算商品推荐列表的耗时操作Thread.sleep(3000);List<String> recommendations = Arrays.asList("商品A", "商品B", "商品C");return recommendations;});// 主线程可以继续执行其他操作System.out.println("主线程继续执行其他任务");try {List<String> recommendations = future.get();System.out.println("商品推荐列表: " + recommendations);} catch (InterruptedException | ExecutionException e) {e.printStackTrace();} finally {executor.shutdown();}}}
上述代码中,首先创建了一个线程池 ExecutorService,并提交了一个 Callable 任务,该任务返回一个 Future 对象。在主线程中,我们可以继续执行其他操作,然后通过调用 Future 对象的 get 方法来获取异步任务的执行结果。如果任务尚未完成,get 方法会阻塞当前线程,直到任务完成并返回结果。
Future 接口的出现,使得 Java 的异步编程更加规范和易于管理,极大地提高了程序的并发性能和响应能力。但它也并非完美无缺,在处理复杂的异步任务组合和回调时,Future 接口的局限性逐渐显现,代码可能会变得冗长和难以维护。
三、CompletableFuture 全面解析
(一)CompletableFuture 的概述
CompletableFuture 是 Java 8 引入的一个强大的异步编程工具,它极大地简化了异步编程的复杂性。它实现了 Future 接口,同时还实现了 CompletionStage 接口,这使得它不仅具备了 Future 的基本功能,还提供了更丰富的异步操作和任务编排能力。
与传统的 Future 相比,CompletableFuture 的最大优势在于它的非阻塞性和对异步任务结果的灵活处理。在传统的 Future 中,我们获取任务结果时通常需要调用get()方法,这个方法会阻塞当前线程,直到任务完成并返回结果。而 CompletableFuture 则通过回调机制,允许我们在任务完成时自动触发后续操作,无需阻塞线程,从而提高了程序的并发性能和响应速度。此外,CompletableFuture 还支持链式调用和函数式编程风格,使得代码更加简洁、易读。
(二)核心特性
- 异步执行任务:CompletableFuture 提供了两个静态方法supplyAsync和runAsync来开启异步任务。supplyAsync方法接受一个Supplier接口的实现,用于返回一个异步计算的结果;runAsync方法接受一个Runnable接口的实现,用于执行一个没有返回值的异步任务。这两个方法都有两个重载版本,一个是使用默认的线程池(ForkJoinPool.commonPool ()),另一个是可以指定自定义的线程池。
import java.util.concurrent.CompletableFuture;import java.util.concurrent.ExecutionException;public class CompletableFutureDemo {public static void main(String[] args) throws ExecutionException, InterruptedException {// 使用supplyAsync方法开启一个异步任务,返回一个CompletableFuture对象CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {// 模拟耗时操作try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}return "任务1的结果";});// 使用runAsync方法开启一个异步任务,返回一个CompletableFuture<Void>对象CompletableFuture<Void> future2 = CompletableFuture.runAsync(() -> {// 模拟耗时操作try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("任务2执行完毕");});// 获取future1的结果,如果任务未完成,get方法会阻塞当前线程String result1 = future1.get();System.out.println("任务1的结果是: " + result1);// 等待future2完成,因为future2没有返回值,所以不需要调用get方法获取结果future2.join();}}
在上述代码中,supplyAsync方法创建了一个异步任务,该任务在后台线程中执行,模拟了一个耗时 2 秒的操作,并返回一个结果。runAsync方法创建了另一个异步任务,同样模拟了一个耗时 1 秒的操作,但没有返回值,只是在任务完成时打印一条消息。主线程在创建这两个异步任务后,不会被阻塞,可以继续执行其他操作。当需要获取supplyAsync任务的结果时,调用get方法,此时如果任务尚未完成,主线程会被阻塞,直到任务完成并返回结果。而对于runAsync任务,由于不需要获取其返回值,所以可以使用join方法等待任务完成,join方法和get方法类似,但它不会抛出受检异常。
- 链式调用:CompletableFuture 支持链式调用,通过thenApply、thenAccept、thenRun等方法,可以对异步任务的结果进行处理,并将处理结果传递给下一个阶段的任务。这种链式调用的方式使得代码更加简洁、易读,并且能够清晰地表达任务之间的依赖关系。
import java.util.concurrent.CompletableFuture;import java.util.concurrent.ExecutionException;public class CompletableFutureChainDemo {public static void main(String[] args) throws ExecutionException, InterruptedException {CompletableFuture.supplyAsync(() -> {System.out.println("任务1执行中...");return "任务1的结果";}).thenApply(result -> {System.out.println("任务2执行中,处理任务1的结果: " + result);return result + " 经过任务2的处理";}).thenAccept(finalResult -> {System.out.println("任务3执行中,最终结果是: " + finalResult);});// 主线程需要等待一段时间,以便异步任务有足够的时间完成Thread.sleep(2000);}}
在这段代码中,首先使用supplyAsync方法创建了一个异步任务,该任务返回一个结果。然后通过thenApply方法,接收上一个任务的结果,并对其进行处理,返回一个新的结果。最后使用thenAccept方法,接收thenApply处理后的结果,并进行消费,但不返回新的结果。整个过程通过链式调用的方式,将三个任务串联起来,形成了一个异步任务链。需要注意的是,由于这些异步任务是在后台线程中执行的,主线程在创建完任务链后,会继续执行后续代码。为了让异步任务有足够的时间完成,这里使用Thread.sleep(2000)方法让主线程睡眠 2 秒。
- 组合异步操作:在实际应用中,我们常常需要将多个异步任务组合起来,以实现更复杂的业务逻辑。CompletableFuture 提供了thenCombine、thenAcceptBoth等方法,用于组合多个异步任务。
import java.util.concurrent.CompletableFuture;import java.util.concurrent.ExecutionException;public class CompletableFutureCombineDemo {public static void main(String[] args) throws ExecutionException, InterruptedException {CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {System.out.println("任务A执行中...");try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}return "任务A的结果";});CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {System.out.println("任务B执行中...");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}return "任务B的结果";});CompletableFuture<String> combinedFuture = future1.thenCombine(future2, (result1, result2) -> {System.out.println("组合任务执行中,合并任务A和任务B的结果");return result1 + " 和 " + result2;});String combinedResult = combinedFuture.get();System.out.println("最终组合结果是: " + combinedResult);}}
上述代码中,首先创建了两个异步任务future1和future2,分别模拟了耗时 2 秒和 1 秒的操作,并返回各自的结果。然后使用thenCombine方法将这两个任务组合起来,thenCombine方法接受另一个CompletableFuture对象和一个BiFunction函数,当future1和future2都完成时,会将它们的结果作为参数传递给BiFunction函数,该函数对两个结果进行合并,并返回一个新的结果,这个新的结果会被包装成一个新的CompletableFuture对象combinedFuture。最后通过get方法获取combinedFuture的结果,即两个任务结果的组合。
- 异常处理:在异步编程中,异常处理是非常重要的一环。CompletableFuture 提供了exceptionally和handle方法,用于优雅地处理异步任务中可能出现的异常。
import java.util.concurrent.CompletableFuture;public class CompletableFutureExceptionDemo {public static void main(String[] args) {CompletableFuture.supplyAsync(() -> {System.out.println("任务执行中...");if (Math.random() > 0.5) {throw new RuntimeException("任务执行出现异常");}return "任务正常结果";}).exceptionally(ex -> {System.out.println("捕获到异常: " + ex.getMessage());return "默认结果";}).thenAccept(result -> {System.out.println("最终结果是: " + result);});// 主线程需要等待一段时间,以便异步任务有足够的时间完成try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}}}
在这段代码中,supplyAsync方法创建的异步任务中,通过Math.random()方法随机生成一个数,如果这个数大于 0.5,则抛出一个运行时异常,否则返回正常结果。exceptionally方法用于捕获异步任务中抛出的异常,当捕获到异常时,会执行exceptionally方法中的处理逻辑,返回一个默认结果。如果异步任务正常完成,exceptionally方法不会被执行,而是直接执行thenAccept方法,处理正常的结果。同样,为了让异步任务有足够的时间完成,主线程通过Thread.sleep(2000)方法睡眠 2 秒 。
四、代码示例与实践
(一)创建异步任务
在使用 CompletableFuture 进行异步编程时,创建异步任务是基础操作。我们可以使用supplyAsync和runAsync方法来创建异步任务,这两个方法都有使用默认线程池和自定义线程池的重载版本。
使用默认线程池创建带有返回值的异步任务,示例如下:
import java.util.concurrent.CompletableFuture;import java.util.concurrent.ExecutionException;public class CompletableFutureCreateTask {public static void main(String[] args) throws ExecutionException, InterruptedException {CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {// 模拟异步任务中的耗时操作,比如查询数据库、调用远程接口等try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}return 42;});// 获取异步任务的执行结果,如果任务尚未完成,get方法会阻塞当前线程int result = future.get();System.out.println("任务的结果是: " + result);}}
在上述代码中,CompletableFuture.supplyAsync方法接受一个Supplier接口的实现,以 Lambda 表达式的形式定义了异步任务的逻辑。在这个任务中,通过Thread.sleep(2000)模拟了一个耗时 2 秒的操作,然后返回结果 42。主线程通过future.get()方法获取异步任务的执行结果,此时如果任务尚未完成,get方法会阻塞主线程,直到任务完成并返回结果。
接下来,我们看一下使用自定义线程池创建异步任务的示例:
import java.util.concurrent.*;public class CompletableFutureCustomThreadPool {public static void main(String[] args) throws ExecutionException, InterruptedException {// 创建一个自定义线程池,设置核心线程数为5,最大线程数为10,线程空闲时间为60秒,任务队列容量为100ThreadPoolExecutor executor = new ThreadPoolExecutor(5,10,60,TimeUnit.SECONDS,new ArrayBlockingQueue<>(100));CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {// 模拟异步任务中的耗时操作,比如复杂的计算逻辑try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}return "自定义线程池执行的任务结果";}, executor);// 获取异步任务的执行结果String result = future.get();System.out.println("任务的结果是: " + result);// 关闭线程池,释放资源executor.shutdown();}}
在这个示例中,我们首先创建了一个ThreadPoolExecutor类型的自定义线程池,设置了核心线程数、最大线程数、线程空闲时间和任务队列容量等参数。然后,使用CompletableFuture.supplyAsync方法并传入自定义线程池,定义了一个异步任务。在任务中同样模拟了一个耗时 3 秒的操作,并返回结果。最后,在主线程中获取任务结果,并在任务完成后关闭线程池,以释放资源。
使用runAsync方法创建没有返回值的异步任务,使用默认线程池的示例如下:
import java.util.concurrent.CompletableFuture;public class CompletableFutureRunAsyncDefault {public static void main(String[] args) {CompletableFuture.runAsync(() -> {// 模拟异步任务中的操作,比如打印日志、更新缓存等try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("没有返回值的异步任务执行完成");});System.out.println("主线程继续执行其他任务");}}
在这段代码中,CompletableFuture.runAsync方法接受一个Runnable接口的实现,定义了一个没有返回值的异步任务。任务中通过Thread.sleep(1000)模拟了一个耗时 1 秒的操作,然后打印任务完成的消息。由于runAsync方法创建的任务没有返回值,所以主线程不需要等待任务完成,可以直接继续执行后续操作。
使用自定义线程池创建没有返回值的异步任务示例如下:
import java.util.concurrent.*;public class CompletableFutureRunAsyncCustom {public static void main(String[] args) {ThreadPoolExecutor executor = new ThreadPoolExecutor(3,6,30,TimeUnit.SECONDS,new LinkedBlockingQueue<>(50));CompletableFuture.runAsync(() -> {// 模拟异步任务中的操作,比如发送邮件、调用第三方接口等try {Thread.sleep(1500);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("使用自定义线程池的无返回值异步任务执行完成");}, executor);System.out.println("主线程继续执行其他任务");// 关闭线程池,释放资源executor.shutdown();}}
此示例中,我们创建了一个自定义线程池,并使用CompletableFuture.runAsync方法结合自定义线程池创建了一个没有返回值的异步任务。任务执行完毕后,打印相应的消息,主线程继续执行其他操作,最后关闭线程池。
(二)链式调用与结果处理
CompletableFuture 的链式调用功能使得我们可以对异步任务的结果进行一系列的处理,让代码更加简洁和易读。通过thenApply、thenAccept、thenRun等方法,我们可以实现对异步任务结果的转换、消费和后续操作。
thenApply方法用于对异步任务的结果进行转换,它接收一个Function函数,将前一个任务的结果作为参数传入该函数,并返回一个新的结果。示例代码如下:
import java.util.concurrent.CompletableFuture;import java.util.concurrent.ExecutionException;public class CompletableFutureThenApply {public static void main(String[] args) throws ExecutionException, InterruptedException {CompletableFuture.supplyAsync(() -> {System.out.println("任务1执行中...");return 10;}).thenApply(result -> {System.out.println("任务2执行中,处理任务1的结果: " + result);return result * 2;}).thenApply(finalResult -> {System.out.println("任务3执行中,处理任务2的结果: " + finalResult);return finalResult + 5;}).thenAccept(result -> {System.out.println("最终结果是: " + result);});// 主线程需要等待一段时间,以便异步任务有足够的时间完成Thread.sleep(2000);}}
在上述代码中,首先使用supplyAsync方法创建了一个异步任务,返回值为 10。然后通过thenApply方法,将任务 1 的结果乘以 2,得到新的结果 20。接着再次使用thenApply方法,将 20 加上 5,得到最终结果 25。最后通过thenAccept方法,消费最终结果并打印输出。整个过程通过链式调用,将多个异步任务串联起来,实现了对结果的逐步处理。
thenAccept方法用于消费异步任务的结果,它接收一个Consumer函数,将前一个任务的结果作为参数传入该函数,但不返回新的结果。示例如下:
import java.util.concurrent.CompletableFuture;import java.util.concurrent.ExecutionException;public class CompletableFutureThenAccept {public static void main(String[] args) throws ExecutionException, InterruptedException {CompletableFuture.supplyAsync(() -> {System.out.println("任务1执行中...");return "Hello";}).thenAccept(result -> {System.out.println("任务2执行中,消费任务1的结果: " + result);// 可以在这个方法中进行一些基于结果的操作,比如打印日志、更新数据库等});// 主线程需要等待一段时间,以便异步任务有足够的时间完成Thread.sleep(1000);}}
在这个例子中,supplyAsync方法创建的异步任务返回字符串 "Hello"。thenAccept方法接收这个结果,并在其内部消费该结果,打印出消费信息。在thenAccept方法中,我们可以根据业务需求进行一些基于结果的操作,比如将结果写入日志文件、更新数据库记录等。
thenRun方法则是在异步任务完成后执行一个无参数的操作,它不关心前一个任务的结果。示例代码如下:
import java.util.concurrent.CompletableFuture;import java.util.concurrent.ExecutionException;public class CompletableFutureThenRun {public static void main(String[] args) throws ExecutionException, InterruptedException {CompletableFuture.supplyAsync(() -> {System.out.println("任务1执行中...");return 100;}).thenRun(() -> {System.out.println("任务2执行中,不依赖任务1的结果");// 这里可以执行一些与前一个任务结果无关的操作,比如发送通知等});// 主线程需要等待一段时间,以便异步任务有足够的时间完成Thread.sleep(1000);}}
在这段代码中,supplyAsync方法创建的异步任务返回值为 100,但thenRun方法并不关心这个结果,它只是在任务 1 完成后执行一个独立的操作,打印出相应的消息。在实际应用中,thenRun方法可以用于执行一些与前一个任务结果无关的操作,比如发送邮件通知、更新系统状态等。
(三)组合异步操作
在实际的开发场景中,我们常常需要将多个异步任务组合起来,以实现更复杂的业务逻辑。CompletableFuture 提供了thenCombine、thenAcceptBoth等方法,方便我们对多个异步任务进行组合操作。
thenCombine方法用于将两个异步任务的结果进行合并,它接收另一个CompletableFuture对象和一个BiFunction函数。当两个异步任务都完成时,BiFunction函数会将两个任务的结果作为参数进行处理,并返回一个新的结果。示例代码如下:
import java.util.concurrent.CompletableFuture;import java.util.concurrent.ExecutionException;public class CompletableFutureThenCombine {public static void main(String[] args) throws ExecutionException, InterruptedException {CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {System.out.println("任务A执行中...");try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}return 10;});CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {System.out.println("任务B执行中...");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}return 20;});CompletableFuture<Integer> combinedFuture = future1.thenCombine(future2, (result1, result2) -> {System.out.println("组合任务执行中,合并任务A和任务B的结果");return result1 + result2;});int combinedResult = combinedFuture.get();System.out.println("最终组合结果是: " + combinedResult);}}
在上述代码中,首先创建了两个异步任务future1和future2,分别模拟了耗时 2 秒和 1 秒的操作,并返回各自的结果 10 和 20。然后使用thenCombine方法将这两个任务组合起来,当future1和future2都完成时,BiFunction函数会将它们的结果相加,得到新的结果 30。最后通过get方法获取组合任务的结果并打印输出。
thenAcceptBoth方法则是在两个异步任务都完成后,将它们的结果作为参数传递给一个BiConsumer函数进行消费,但不返回新的结果。示例如下:
import java.util.concurrent.CompletableFuture;import java.util.concurrent.ExecutionException;public class CompletableFutureThenAcceptBoth {public static void main(String[] args) throws ExecutionException, InterruptedException {CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {System.out.println("任务X执行中...");try {Thread.sleep(1500);} catch (InterruptedException e) {e.printStackTrace();}return "Hello";});CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {System.out.println("任务Y执行中...");try {Thread.sleep(2500);} catch (InterruptedException e) {e.printStackTrace();}return "World";});future1.thenAcceptBoth(future2, (result1, result2) -> {System.out.println("组合任务执行中,消费任务X和任务Y的结果");System.out.println("拼接后的结果是: " + result1 + " " + result2);});// 主线程需要等待一段时间,以便异步任务有足够的时间完成Thread.sleep(3000);}}
在这个例子中,future1和future2分别是两个异步任务,完成后分别返回 "Hello" 和 "World"。thenAcceptBoth方法接收这两个任务的结果,并通过BiConsumer函数将它们拼接起来并打印输出。在thenAcceptBoth方法中,我们可以根据业务需求对两个任务的结果进行各种消费操作,比如将结果写入文件、发送到消息队列等。
(四)异常处理
在异步编程中,异常处理是至关重要的环节。CompletableFuture 提供了exceptionally和handle方法,帮助我们优雅地处理异步任务中可能出现的异常。
exceptionally方法用于捕获异步任务执行过程中抛出的异常,并返回一个默认值或处理后的结果。示例代码如下:
import java.util.concurrent.CompletableFuture;import java.util.concurrent.ExecutionException;public class CompletableFutureExceptionally {public static void main(String[] args) {CompletableFuture.supplyAsync(() -> {System.out.println("任务执行中...");if (Math.random() > 0.5) {throw new RuntimeException("任务执行出现异常");}return "任务正常结果";}).exceptionally(ex -> {System.out.println("捕获到异常: " + ex.getMessage());return "默认结果";}).thenAccept(result -> {System.out.println("最终结果是: " + result);});// 主线程需要等待一段时间,以便异步任务有足够的时间完成try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}}}
在上述代码中,supplyAsync方法创建的异步任务中,通过Math.random()方法随机生成一个数,如果这个数大于 0.5,则抛出一个运行时异常,否则返回正常结果。exceptionally方法用于捕获异步任务中抛出的异常,当捕获到异常时,会执行exceptionally方法中的处理逻辑,打印异常信息并返回一个默认结果。如果异步任务正常完成,exceptionally方法不会被执行,而是直接执行thenAccept方法,处理正常的结果。
handle方法则更加灵活,它不仅可以处理异常,还可以处理正常的计算结果。handle方法接收一个BiFunction函数,该函数接收两个参数:异步任务的结果和可能出现的异常。根据这两个参数,我们可以返回不同的处理结果。示例如下:
import java.util.concurrent.CompletableFuture;import java.util.concurrent.ExecutionException;public class CompletableFutureHandle {public static void main(String[] args) {CompletableFuture.supplyAsync(() -> {System.out.println("任务执行中...");if (Math.random() > 0.3) {throw new RuntimeException("任务执行出现异常");}return "任务正常结果";}).handle((result, ex) -> {if (ex!= null) {System.out.println("捕获到异常: " + ex.getMessage());return "异常处理后的结果";}System.out.println("任务正常完成,结果是: " + result);return result;}).thenAccept(finalResult -> {System.out.println("最终结果是: " + finalResult);});// 主线程需要等待一段时间,以便异步任务有足够的时间完成try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}}}
在这个例子中,supplyAsync方法创建的异步任务同样可能会抛出异常。handle方法中的BiFunction函数会根据任务的执行情况进行处理。如果出现异常,会打印异常信息并返回异常处理后的结果;如果任务正常完成,会打印正常结果并返回该结果。最后通过thenAccept方法处理最终的
五、与其他异步编程方式的对比
(一)与 Future 的对比
在 Java 的异步编程领域,Future 接口是早期的重要工具,而 CompletableFuture 则是在其基础上的进一步发展和增强。下面我们从功能、性能、灵活性等方面来对比这两者,看看 CompletableFuture 究竟有哪些独特的优势。
从功能层面来看,Future 主要用于表示异步计算的结果,它提供了isDone方法用于检查计算是否完成,get方法用于获取计算结果,以及cancel方法用于取消计算任务。然而,Future 的功能相对较为单一,获取结果时,如果计算尚未完成,get方法会阻塞当前线程,这在一定程度上限制了其在复杂异步场景中的应用。例如,在一个需要同时处理多个异步任务并对结果进行复杂处理的电商系统中,使用 Future 可能会导致主线程长时间阻塞,影响系统的响应性能。
相比之下,CompletableFuture 的功能要丰富得多。它不仅实现了 Future 接口,具备 Future 的基本功能,还提供了大量用于异步任务编排和结果处理的方法。通过thenApply、thenAccept、thenRun等方法,我们可以方便地对异步任务的结果进行转换、消费和后续操作,实现链式调用,使代码更加简洁和易读。在处理多个异步任务时,CompletableFuture 提供了thenCombine、thenAcceptBoth、allOf、anyOf等方法,能够轻松地实现任务的组合和并行执行,满足各种复杂的业务需求。
在性能方面,由于 Future 的get方法会阻塞线程,当有大量异步任务需要处理时,可能会导致线程资源的浪费和系统性能的下降。而 CompletableFuture 采用了非阻塞的方式来处理异步结果,通过回调机制,在任务完成时自动触发后续操作,无需阻塞线程,从而提高了系统的并发性能和响应速度。例如,在一个高并发的网络爬虫系统中,使用 CompletableFuture 可以充分利用线程资源,同时处理多个网页的抓取和解析任务,大大提高了爬虫的效率。
灵活性上,Future 的操作相对较为固定,缺乏对多个异步操作的组合能力。而 CompletableFuture 则提供了强大的组合和处理能力,可以方便地处理复杂的异步任务。它支持在任务执行过程中动态地添加回调函数,根据任务的执行结果或异常情况进行不同的处理,使得异步编程更加灵活和可控。在一个分布式系统中,可能需要调用多个不同的服务来完成一个业务流程,使用 CompletableFuture 可以轻松地将这些异步调用组合起来,实现高效的分布式事务处理。
(二)与其他异步框架的对比
除了与 Future 进行对比,我们也来简要提及一下 CompletableFuture 与其他异步框架的差异,比如 Guava 的 ListenableFuture。
Guava 的 ListenableFuture 是对 Java 原生 Future 的扩展,它提供了一种可以监听异步任务完成的机制,通过添加回调函数,在任务完成时自动触发回调操作,这在一定程度上弥补了 Future 的不足。例如,在一个需要实时处理消息的系统中,使用 ListenableFuture 可以在消息处理完成后立即触发通知操作,提高系统的实时性。
然而,CompletableFuture 作为 Java 标准库的一部分,在使用和集成上具有独特的便利性。它无需额外引入第三方库,减少了项目的依赖,降低了维护成本。而且,CompletableFuture 与 Java 8 引入的 Lambda 表达式、Stream API 等新特性紧密结合,能够更好地利用 Java 语言的新特性进行异步编程,使得代码更加简洁、高效。在使用 CompletableFuture 时,我们可以直接使用 Lambda 表达式来定义异步任务和回调函数,利用 Stream API 对多个异步任务进行并行处理和结果汇总,大大提高了开发效率。
在功能方面,CompletableFuture 同样具有一定的优势。它提供了更丰富的方法来处理异步任务的组合、异常处理和超时控制等。在处理多个异步任务的组合时,CompletableFuture 的thenCombine、allOf、anyOf等方法比 ListenableFuture 更加简洁和灵活,能够更方便地实现复杂的业务逻辑。在异常处理方面,CompletableFuture 的exceptionally和handle方法提供了更强大的异常处理能力,能够更优雅地处理异步任务中可能出现的异常情况。
六、实际应用场景
(一)电商系统中的商品比价
在电商领域,用户在购买商品时往往希望能够快速了解不同平台上同款商品的价格,以便做出最优的购买决策。为了实现这一功能,我们可以利用 CompletableFuture 来异步地从多个电商平台获取商品价格信息,并进行比价展示。
假设我们有一个电商比价系统,需要从淘宝、京东、拼多多等多个电商平台获取某款手机的价格。使用 CompletableFuture,我们可以将每个平台的价格查询操作封装成一个异步任务,然后并行地执行这些任务,最后将所有平台的价格信息汇总展示给用户。示例代码如下:
import java.util.ArrayList;import java.util.List;import java.util.concurrent.CompletableFuture;import java.util.concurrent.ExecutionException;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.stream.Collectors;// 定义电商平台类class EcommercePlatform {private String platformName;public EcommercePlatform(String platformName) {this.platformName = platformName;}// 模拟从电商平台获取商品价格的方法public double getProductPrice(String product) {// 这里可以替换为实际的价格查询逻辑,比如调用电商平台的APItry {Thread.sleep(1000); // 模拟网络延迟或数据查询耗时} catch (InterruptedException e) {e.printStackTrace();}// 随机生成一个价格作为示例return Math.random() * 1000 + 1000;}}public class PriceComparisonSystem {public static void main(String[] args) throws ExecutionException, InterruptedException {// 创建线程池ExecutorService executor = Executors.newFixedThreadPool(3);// 定义要查询价格的商品String product = "iPhone 15";// 定义各个电商平台EcommercePlatform taobao = new EcommercePlatform("淘宝");EcommercePlatform jd = new EcommercePlatform("京东");EcommercePlatform pinduoduo = new EcommercePlatform("拼多多");// 使用CompletableFuture异步获取各个平台的商品价格CompletableFuture<Double> taobaoPriceFuture = CompletableFuture.supplyAsync(() -> taobao.getProductPrice(product), executor);CompletableFuture<Double> jdPriceFuture = CompletableFuture.supplyAsync(() -> jd.getProductPrice(product), executor);CompletableFuture<Double> pinduoduoPriceFuture = CompletableFuture.supplyAsync(() -> pinduoduo.getProductPrice(product), executor);// 将所有的CompletableFuture放入一个列表中List<CompletableFuture<Double>> futureList = new ArrayList<>();futureList.add(taobaoPriceFuture);futureList.add(jdPriceFuture);futureList.add(pinduoduoPriceFuture);// 使用CompletableFuture.allOf等待所有异步任务完成CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0])).join();// 获取各个平台的商品价格List<Double> priceList = futureList.stream().map(CompletableFuture::join).collect(Collectors.toList());// 打印各个平台的商品价格System.out.println("商品 " + product + " 在不同平台的价格如下:");System.out.println("淘宝:" + priceList.get(0));System.out.println("京东:" + priceList.get(1));System.out.println("拼多多:" + priceList.get(2));// 关闭线程池executor.shutdown();}}
在上述代码中,首先创建了一个固定大小的线程池,用于执行异步任务。然后,定义了三个电商平台对象,并使用 CompletableFuture 的supplyAsync方法将每个平台的价格查询操作封装成异步任务,这些任务将在后台线程中并行执行。通过CompletableFuture.allOf方法等待所有异步任务完成,最后获取各个平台的商品价格并打印输出。使用 CompletableFuture 实现商品比价功能,大大提高了获取价格信息的效率,避免了因顺序查询而导致的长时间等待,为用户提供了更快速、便捷的购物体验。
(二)多数据源的数据获取与整合
在企业级开发中,经常会遇到需要从多个数据源获取数据并进行整合处理的场景。例如,在一个大型企业的数据分析系统中,用户信息可能存储在关系型数据库中,而用户的行为数据则存储在分布式缓存和日志文件中。为了生成全面的用户分析报告,我们需要从这些不同的数据源获取数据,并进行整合和分析。
假设我们要开发一个用户数据分析模块,需要从数据库中获取用户的基本信息,从缓存中获取用户的近期登录记录,从日志文件中获取用户的操作行为数据。使用 CompletableFuture,我们可以将这些数据获取操作并行化,提高数据获取的效率。示例代码如下:
import java.util.concurrent.CompletableFuture;import java.util.concurrent.ExecutionException;// 模拟数据库操作类class DatabaseService {public String getUserInfo(String userId) {try {Thread.sleep(1500); // 模拟数据库查询耗时} catch (InterruptedException e) {e.printStackTrace();}return "用户基本信息:姓名:张三,年龄:25,性别:男";}}// 模拟缓存操作类class CacheService {public String getLoginRecords(String userId) {try {Thread.sleep(1000); // 模拟缓存查询耗时} catch (InterruptedException e) {e.printStackTrace();}return "近期登录记录:[2024-01-01 10:00:00,2024-01-02 15:30:00]";}}// 模拟日志文件操作类class LogService {public String getOperationLogs(String userId) {try {Thread.sleep(2000); // 模拟日志文件读取耗时} catch (InterruptedException e) {e.printStackTrace();}return "操作行为数据:[查看商品详情,添加购物车,提交订单]";}}public class UserDataAnalysis {public static void main(String[] args) throws ExecutionException, InterruptedException {String userId = "123456";DatabaseService databaseService = new DatabaseService();CacheService cacheService = new CacheService();LogService logService = new LogService();// 使用CompletableFuture异步获取用户基本信息CompletableFuture<String> userInfoFuture = CompletableFuture.supplyAsync(() -> databaseService.getUserInfo(userId));// 使用CompletableFuture异步获取用户近期登录记录CompletableFuture<String> loginRecordsFuture = CompletableFuture.supplyAsync(() -> cacheService.getLoginRecords(userId));// 使用CompletableFuture异步获取用户操作行为数据CompletableFuture<String> operationLogsFuture = CompletableFuture.supplyAsync(() -> logService.getOperationLogs(userId));// 使用CompletableFuture.allOf等待所有异步任务完成CompletableFuture.allOf(userInfoFuture, loginRecordsFuture, operationLogsFuture).join();// 获取并整合数据String userInfo = userInfoFuture.get();String loginRecords = loginRecordsFuture.get();String operationLogs = operationLogsFuture.get();String combinedData = "用户ID:" + userId + "\n" + userInfo + "\n" + loginRecords + "\n" + operationLogs;System.out.println(combinedData);}}
在这个示例中,分别创建了模拟数据库、缓存和日志文件操作的类,并定义了相应的数据获取方法。使用 CompletableFuture 的supplyAsync方法将从不同数据源获取数据的操作封装成异步任务,这些任务会在后台线程中并行执行。通过CompletableFuture.allOf方法等待所有异步任务完成,然后获取各个数据源的数据并进行整合,最终输出完整的用户分析数据。这种方式充分利用了 CompletableFuture 的异步和并行处理能力,大大提高了数据获取和整合的效率,为企业级应用的高效运行提供了有力支持 。
七、总结与展望
在 Java 异步编程的广阔领域中,CompletableFuture 无疑占据着举足轻重的地位。它的出现,为开发者们提供了一种高效、便捷且灵活的异步编程解决方案,极大地简化了异步任务的处理流程。
从基本的异步任务创建,到复杂的任务组合与结果处理,再到优雅的异常处理机制,CompletableFuture 凭借其丰富的方法和强大的功能,满足了各种场景下的异步编程需求。与传统的异步编程方式相比,它的优势显而易见,无论是在提升代码的可读性、可维护性,还是在提高程序的性能和响应性方面,都表现出色。
在实际项目中,我们已经看到 CompletableFuture 在电商系统、企业级数据处理等诸多领域发挥了重要作用。它能够帮助我们更高效地利用系统资源,提升用户体验,增强系统的竞争力。
展望未来,随着 Java 技术的不断发展和应用场景的日益丰富,相信 CompletableFuture 还将不断进化和完善,为我们带来更多的惊喜和便利。同时,也希望广大读者能够在日常的开发工作中,大胆地使用 CompletableFuture,不断探索它的更多可能性,让我们的程序在异步编程的加持下,运行得更加高效、流畅 。