一、前言
Java异步编程从Thread、Runnable一路演进到Future,但传统方案始终存在难以解决的痛点。而CompletableFuture的出现,让Java异步编程进入了更灵活、更高效的阶段。本篇作为系列的入门篇,将先带大家理清传统异步编程的局限,再深入讲解CompletableFuture的核心优势与基础使用方法,帮你快速建立对CompletableFuture的认知。
二、传统异步编程的痛点
在实际开发中,我们经常会遇到需要“并行处理任务”的场景,比如查询用户信息的同时加载用户订单数据,或者批量调用多个第三方接口获取数据。为了避免同步执行导致的性能低下,异步编程成为了必然选择。但在CompletableFuture出现之前,Java的异步方案都存在明显的不足。
1、Thread+Runnable的局限性
最早期的异步实现是通过Thread配合Runnable接口完成的。Runnable接口定义了一个无返回值、无异常抛出的run()方法,我们可以在其中封装异步任务逻辑。
示例代码如下:
public class ThreadRunnableDemo {
public static void main(String[] args) {
// 异步执行任务
new Thread(new Runnable() {
@Override
public void run() {
// 模拟耗时操作:查询用户订单
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("异步任务执行完成:查询到用户订单数据");
}
}).start();
// 主线程继续执行其他操作
System.out.println("主线程执行完毕");
}
}
运行结果:
主线程执行完毕
异步任务执行完成:查询到用户订单数据
从示例中可以看出,Thread+Runnable确实实现了异步执行,但它的局限性非常明显:
-
无法获取任务返回值:Runnable的run()方法无返回值,若异步任务需要产生结果(比如查询到的订单数据),无法直接传递给主线程,只能通过共享变量等间接方式,操作繁琐且容易引发线程安全问题。
-
难以管理线程资源:每次创建新Thread会消耗系统资源,频繁创建线程会导致系统负载过高;且线程缺乏统一管理,无法控制并发数,可能引发资源耗尽问题。
-
无异常处理机制:run()方法不允许抛出checked异常,只能在方法内部捕获处理;若发生未捕获异常,线程会直接终止,主线程无法感知。
2、Future的核心能力与不足
为了解决Runnable无法获取返回值的问题,Java 5引入了Callable接口和Future接口。Callable接口的call()方法支持返回值,且可以抛出异常;Future接口则用于接收Callable任务的执行结果,相当于一个“异步任务的结果占位符”。
同时,Java还提供了ExecutorService线程池框架,用于统一管理线程资源,避免频繁创建线程的问题。我们可以通过线程池提交Callable任务,获取Future对象,再通过Future获取任务结果。
示例代码如下:
import java.util.concurrent.*;
public class FutureDemo {
// 创建固定大小的线程池
private static final ExecutorService executor = Executors.newFixedThreadPool(2);
public static void main(String[] args) {
// 1. 提交Callable任务,获取Future对象
Future<String> future = executor.submit(new Callable<String>() {
@Override
public String call() throws Exception {
// 模拟耗时操作:查询用户订单
Thread.sleep(1000);
return "用户订单数据:订单号123,金额100元";
}
});
// 2. 主线程执行其他操作
System.out.println("主线程执行其他任务…");
// 3. 获取异步任务结果
try {
// get()方法会阻塞主线程,直到任务完成
String result = future.get();
System.out.println("获取到异步任务结果:" + result);
} catch (InterruptedException e) {
// 线程被中断的异常处理
e.printStackTrace();
} catch (ExecutionException e) {
// 异步任务执行过程中抛出的异常
e.printStackTrace();
} finally {
// 关闭线程池
executor.shutdown();
}
}
运行结果:
主线程执行其他任务…
获取到异步任务结果:用户订单数据:订单号123,金额100元
相比Thread+Runnable,Future的进步是显著的: 支持获取任务返回值 、 可通过线程池管理线程资源 、 能捕获异步任务抛出的异常。但在实际复杂场景中,Future的不足依然突出:
-
get()方法阻塞问题:如果异步任务还未完成,调用get()方法会直接阻塞主线程,直到任务完成。这会导致异步编程的优势大打折扣,甚至退化为同步执行。虽然Future提供了带超时参数的get(long timeout, TimeUnit unit)方法,但超时后会抛出TimeoutException,需要手动处理。
-
缺乏回调机制:Future无法注册回调函数,无法实现“任务完成后自动执行后续操作”。比如,我们希望在订单查询完成后,自动执行订单金额统计,只能通过主线程循环判断Future是否完成(isDone()方法),再手动触发后续操作,代码繁琐且效率低。
-
无法组合多个任务:Future不支持多个异步任务的串行、并行组合。比如,需要先查询用户信息,再根据用户ID查询订单,最后根据订单ID查询物流信息;或者需要并行查询用户的订单和积分,再聚合结果。这些场景用Future实现会非常复杂,需要手动管理多个Future的执行顺序和结果。
-
异常处理不直观:异步任务抛出的异常会被封装在ExecutionException中,需要通过e.getCause()才能获取原始异常,排查问题成本高。
3、实际开发场景的痛点示例
假设我们有一个电商首页的需求:需要同时查询用户的基础信息、待支付订单、会员积分,然后将这三部分数据聚合后返回给前端。如果用Future实现,代码会是这样的:
import java.util.concurrent.*;
public class FutureMultiTaskDemo {
private static final ExecutorService executor = Executors.newFixedThreadPool(3);
// 模拟查询用户基础信息
public static Future<String> queryUserInfo(String userId) {
return executor.submit(() -> {
Thread.sleep(800);
return "用户信息:userId=" + userId + ", 姓名=张三";
});
}
// 模拟查询待支付订单
public static Future<String> queryPendingOrders(String userId) {
return executor.submit(() -> {
Thread.sleep(1000);
return "待支付订单:userId=" + userId + ", 订单数=2";
});
}
// 模拟查询会员积分
public static Future<String> queryUserPoints(String userId) {
return executor.submit(() -> {
Thread.sleep(1200);
return "会员积分:userId=" + userId + ", 积分=1000";
});
}
public static void main(String[] args) {
String userId = "1001";
long start = System.currentTimeMillis();
// 1. 并行提交三个异步任务
Future<String> userInfoFuture = queryUserInfo(userId);
Future<String> ordersFuture = queryPendingOrders(userId);
Future<String> pointsFuture = queryUserPoints(userId);
// 2. 逐个获取结果(此处会阻塞,直到对应任务完成)
try {
String userInfo = userInfoFuture.get();
String orders = ordersFuture.get();
String points = pointsFuture.get();
// 3. 聚合结果
String result = "首页数据聚合:\\n" + userInfo + "\\n" + orders + "\\n" + points;
System.out.println(result);
System.out.println("总耗时:" + (System.currentTimeMillis() – start) + "ms");
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
} finally {
executor.shutdown();
}
}
运行结果:
首页数据聚合:
用户信息:userId=1001, 姓名=张三
待支付订单:userId=1001, 订单数=2
会员积分:userId=1001, 积分=1000
总耗时:1205ms
从结果来看,三个任务并行执行,总耗时接近最长任务的耗时(1200ms),效率尚可。但代码存在两个核心问题:
-
获取结果时的阻塞:虽然三个任务是并行的,但调用get()方法时,若对应任务未完成,主线程依然会阻塞。比如,若userInfoFuture先完成,调用ordersFuture.get()时,主线程会阻塞到订单查询任务完成,再调用pointsFuture.get()时,可能还需要继续阻塞。
-
缺乏异常降级机制:如果其中一个任务执行失败(比如查询积分的接口报错),会直接抛出ExecutionException,导致整个聚合任务失败。但实际业务中,我们可能希望“积分查询失败时,用默认值0填充,不影响其他数据的展示”,这种需求用Future实现会非常繁琐。
而这些问题,CompletableFuture都能给出优雅的解决方案。
三、核心优势
CompletableFuture是Java 8新增的类,它实现了Future接口和CompletionStage接口。其中,Future接口保证了它具备获取异步结果的基础能力;而CompletionStage接口则定义了任务完成后的后续处理逻辑(回调、组合等),这也是CompletableFuture的核心价值所在。
相比传统的Future,CompletableFuture的核心优势主要体现在以下4个方面:
1、非阻塞获取结果,支持回调机制
CompletableFuture无需通过阻塞的get()方法获取结果,而是可以通过注册回调函数(如thenAccept()、thenApply()等),实现“任务完成后自动执行后续操作”。回调函数的执行是异步的,不会阻塞主线程,真正发挥了异步编程的优势。
比如,我们可以给“查询订单”的任务注册一个回调函数,订单查询完成后,自动执行“统计订单金额”的操作,代码简洁且非阻塞。
2、强大的任务组合能力
CompletableFuture提供了丰富的API,支持多个异步任务的串行、并行、依赖组合。比如:
-
串行组合:任务A完成后,自动执行任务B(任务B依赖任务A的结果)。
-
并行组合:等待多个任务全部完成后,聚合结果(如前面的首页数据聚合场景);或任意一个任务完成后,立即返回结果(如调用多个支付渠道,取最快响应的一个)。
-
依赖组合:两个任务都完成后,执行后续操作(如查询用户信息和订单信息都完成后,聚合为完整用户数据)。
这些组合能力无需手动管理任务的执行顺序和结果,极大简化了复杂异步场景的开发。
3、丰富的异常处理机制
CompletableFuture提供了专门的异常处理API(如exceptionally()、handle()等),可以精准捕获异步任务执行过程中的异常,并进行降级处理(如返回默认值、重试任务等)。相比Future需要通过get()方法捕获ExecutionException的方式,CompletableFuture的异常处理更直观、更灵活。
比如,前面的积分查询任务失败时,我们可以通过exceptionally()方法直接返回默认积分0,不影响其他任务的结果聚合。
4、支持自定义线程池
CompletableFuture的所有异步方法(如supplyAsync()、runAsync())都支持传入自定义的Executor(线程池),可以根据业务需求灵活控制线程资源。而传统的Future虽然也能通过线程池提交任务,但CompletableFuture将线程池的配置与任务的定义更紧密地结合起来,使用更便捷。
同时,这也避免了使用默认线程池(ForkJoinPool.commonPool())可能带来的资源竞争问题(后续篇章会详细讲解)。
四、基础使用入门
了解了CompletableFuture的核心优势后,我们从最基础的使用方法开始学习。CompletableFuture的核心构造方法和基础API并不多,掌握这部分内容,就能应对简单的异步场景。
1、核心构造方法
CompletableFuture提供了两个最常用的静态方法来创建异步任务:supplyAsync()和runAsync(),它们的核心区别在于是否有返回值。
1.supplyAsync():创建有返回值的异步任务
supplyAsync()接收一个Supplier函数式接口参数(无参数,返回T类型结果),用于定义有返回值的异步任务。它有两个重载方法:
// 1. 不指定线程池,使用默认线程池(ForkJoinPool.commonPool())
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
// 2. 指定自定义线程池
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)
示例代码:用supplyAsync()异步查询用户订单,并获取返回值
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CompletableFutureSupplyAsyncDemo {
// 自定义线程池
private static final ExecutorService executor = Executors.newFixedThreadPool(2);
public static void main(String[] args) {
// 1. 创建有返回值的异步任务(指定自定义线程池)
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// 模拟耗时操作:查询用户订单
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException("任务执行被中断", e);
}
return "用户订单数据:订单号123,金额100元";
}, executor);
// 2. 注册回调函数:任务完成后自动执行(非阻塞)
future.thenAccept(result -> {
System.out.println("异步任务执行完成,结果:" + result);
});
// 3. 主线程继续执行其他操作
System.out.println("主线程执行完毕,无需等待异步任务");
// 注意:主线程不能立即结束,否则自定义线程池中的任务会被中断
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
executor.shutdown();
}
}
运行结果:
主线程执行完毕,无需等待异步任务
异步任务执行完成,结果:用户订单数据:订单号123,金额100元
从结果可以看出,主线程无需等待异步任务完成,而是直接执行完毕;当异步任务完成后,回调函数thenAccept()会自动执行,打印任务结果。这就是非阻塞回调的核心优势。
2.runAsync():创建无返回值的异步任务
runAsync()接收一个Runnable函数式接口参数(无参数,无返回值),用于定义无返回值的异步任务。它也有两个重载方法:
// 1. 使用默认线程池
public static CompletableFuture<Void> runAsync(Runnable runnable)
// 2. 指定自定义线程池
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)
示例代码:用runAsync()异步记录系统日志(无返回值)
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CompletableFutureRunAsyncDemo {
private static final ExecutorService executor = Executors.newFixedThreadPool(2);
public static void main(String[] args) {
// 1. 创建无返回值的异步任务
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
// 模拟耗时操作:记录系统日志
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("异步任务执行完成:系统日志记录成功");
}, executor);
// 2. 注册回调函数:任务完成后打印提示
future.thenRun(() -> {
System.out.println("回调函数执行:日志记录任务已结束");
});
System.out.println("主线程执行完毕");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
executor.shutdown();
}
}
运行结果:
主线程执行完毕
异步任务执行完成:系统日志记录成功
回调函数执行:日志记录任务已结束
runAsync()适用于不需要返回结果的异步场景,如日志记录、消息推送等。
2、基础获取结果方法
虽然CompletableFuture推荐使用回调函数获取结果,但在某些特殊场景下(如需要同步等待结果聚合),依然可以通过join()或get()方法主动获取任务结果。这两个方法的核心区别在于异常处理。
1.join()方法
join()方法无返回值(若任务有返回值,返回任务结果;无返回值则返回Void),且不会抛出checked异常(即不需要try-catch捕获)。若异步任务执行失败,会直接抛出UncheckedException(运行时异常)。
示例代码:
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CompletableFutureJoinDemo {
private static final ExecutorService executor = Executors.newFixedThreadPool(2);
public static void main(String[] args) {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
if (true) {
throw new RuntimeException("任务执行失败");
}
return "任务执行成功";
}, executor);
// 调用join()方法获取结果,无需try-catch
String result = future.join();
System.out.println(result);
executor.shutdown();
}
运行结果(抛出运行时异常):
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CompletableFutureJoinDemo {
private static final ExecutorService executor = Executors.newFixedThreadPool(2);
public static void main(String[] args) {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
if (true) {
throw new RuntimeException("任务执行失败");
}
return "任务执行成功";
}, executor);
// 调用join()方法获取结果,无需try-catch
String result = future.join();
System.out.println(result);
executor.shutdown();
}
2.get()方法
get()方法与Future的get()方法类似,会阻塞主线程直到任务完成,并返回任务结果。它会抛出两个checked异常,必须通过try-catch捕获:
-
InterruptedException:线程在阻塞等待过程中被中断。
-
ExecutionException:异步任务执行过程中抛出的异常(被封装在ExecutionException中)。
示例代码:
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CompletableFutureGetDemo {
private static final ExecutorService executor = Executors.newFixedThreadPool(2);
public static void main(String[] args) {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
if (true) {
throw new RuntimeException("任务执行失败");
}
return "任务执行成功";
}, executor);
try {
// 必须捕获checked异常
String result = future.get();
System.out.println(result);
} catch (InterruptedException e) {
System.out.println("线程被中断");
e.printStackTrace();
} catch (ExecutionException e) {
System.out.println("异步任务执行失败,原始异常:" + e.getCause().getMessage());
e.printStackTrace();
} finally {
executor.shutdown();
}
}
运行结果:
异步任务执行失败,原始异常:任务执行失败
java.util.concurrent.ExecutionException: java.lang.RuntimeException: 任务执行失败
at java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:357)
at java.util.concurrent.CompletableFuture.get(CompletableFuture.java:1895)
at com.example.CompletableFutureGetDemo.main(CompletableFutureGetDemo.java:15)
Caused by: java.lang.RuntimeException: 任务执行失败
at com.example.CompletableFutureGetDemo.lambda$main$0(CompletableFutureGetDemo.java:11)
at java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1590)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
join()与get()的核心区别总结
|
抛出UncheckedException(运行时异常),无需try-catch |
抛出checked异常(InterruptedException、ExecutionException),必须try-catch |
|
返回任务结果(或Void) |
返回任务结果(或Void) |
|
阻塞主线程直到任务完成 |
阻塞主线程直到任务完成 |
|
不需要精细处理异常的场景,或Lambda表达式中(避免try-catch繁琐) |
需要精细处理异常的场景,明确感知任务失败原因 |
3、CompletableFuture的demo
结合前面的知识点,我们实现一个完整的场景:异步查询用户的基础信息和待支付订单,然后聚合结果返回。这个场景用传统Future实现比较繁琐,但用CompletableFuture的回调和组合能力,代码会非常简洁。
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CompletableFutureFirstDemo {
// 自定义线程池(IO密集型任务,核心线程数设为CPU核心数*2)
private static final ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2);
// 模拟查询用户基础信息
public static CompletableFuture<String> queryUserInfo(String userId) {
return CompletableFuture.supplyAsync(() -> {
// 模拟IO耗时操作
try {
Thread.sleep(800);
} catch (InterruptedException e) {
throw new RuntimeException("查询用户信息失败", e);
}
return "用户信息:userId=" + userId + ", 姓名=张三, 性别=男";
}, executor);
}
// 模拟查询待支付订单
public static CompletableFuture<String> queryPendingOrders(String userId) {
return CompletableFuture.supplyAsync(() -> {
// 模拟IO耗时操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException("查询待支付订单失败", e);
}
return "待支付订单:userId=" + userId + ", 订单号=ORDER123, 金额=200元, 状态=待支付";
}, executor);
}
public static void main(String[] args) {
String userId = "1001";
long start = System.currentTimeMillis();
// 1. 并行执行两个异步任务
CompletableFuture<String> userInfoFuture = queryUserInfo(userId);
CompletableFuture<String> ordersFuture = queryPendingOrders(userId);
// 2. 两个任务都完成后,聚合结果(thenCombine用于组合两个任务的结果)
CompletableFuture<String> combinedFuture = userInfoFuture.thenCombine(ordersFuture, (userInfo, orders) -> {
return "用户完整数据聚合:\\n" + userInfo + "\\n" + orders;
});
// 3. 注册回调函数,获取聚合结果
combinedFuture.thenAccept(combinedResult -> {
System.out.println(combinedResult);
System.out.println("聚合任务总耗时:" + (System.currentTimeMillis() – start) + "ms");
});
// 4. 异常处理:若任意任务失败,打印异常信息
combinedFuture.exceptionally(ex -> {
System.out.println("任务执行失败:" + ex.getCause().getMessage());
return null;
});
// 主线程继续执行其他操作
System.out.println("主线程执行完毕,等待异步任务完成…");
// 防止主线程提前结束,导致线程池被关闭
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
executor.shutdown();
}
}
运行结果:
主线程执行完毕,等待异步任务完成…
用户完整数据聚合:
用户信息:userId=1001, 姓名=张三, 性别=男
待支付订单:userId=1001, 订单号=ORDER123, 金额=200元, 状态=待支付
聚合任务总耗时:1008ms
从结果可以看出:
-
两个任务并行执行,总耗时接近最长任务的耗时(1000ms),效率很高。
-
通过thenCombine()方法轻松实现了两个任务结果的聚合,无需手动管理任务完成顺序。
-
通过exceptionally()方法实现了异常处理,若任意任务失败,会打印失败原因。
-
主线程无需阻塞等待,而是继续执行其他操作,直到异步任务完成后,回调函数自动执行。
五、总结
本篇为我们梳理了传统异步编程方案(Thread+Runnable、Future)的痛点:Thread无法获取返回值,Future的get()方法阻塞、缺乏回调和任务组合能力。随后,我们介绍了CompletableFuture的核心优势:非阻塞回调、强大的任务组合、丰富的异常处理、支持自定义线程池。最后,我们学习了CompletableFuture的基础使用方法,包括核心构造方法supplyAsync()/runAsync()、结果获取方法join()/get(),并通过一个完整的示例展示了它的优势。
网硕互联帮助中心






评论前必须登录!
注册