CompletableFuture
是 Java 8 引入的一个非常强大的类,它提供了一个可扩展的异步编程模型。CompletableFuture
表示一个可能还没有完成的异步计算,也就是说可以异步地执行任务,不阻塞当前线程,它提供了多种方法来组合和转换结果。
CompletableFuture
使用 thenApply
、thenAccept
、thenRun
等方法,可以在计算完成时指定回调函数。
CompletableFuture
可以组合其他 CompletableFuture
对象,例如使用 thenCompose
方法。使用 exceptionally
方法可以处理异步操作中的异常。
CompletableFuture
的方法通常返回 CompletableFuture
类型,支持链式调用。
CompletableFuture
默认使用 ForkJoinPool
线程池,但可以通过 Executor
自定义线程池。
CompletableFuture
有多种完成阶段,如 completed
、failed
和 cancelled
。
如果想要执行同步等待操作,使用 get
方法可以等待异步操作完成,但要注意的是,这可能引发的 InterruptedException
异常。
以上是CompletableFuture
的关键特性,下面我们来看一个业务场景案例,假设我们有一个场景,需要从数据库获取用户信息,然后根据用户信息调用外部服务进行处理,最后返回处理结果。
代码案例:
java
代码解读
复制代码
import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.function.Function; public class CompletableFutureExample { public static void main(String[] args) { // 模拟从数据库异步获取用户信息 CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> { // 模拟数据库查询 return getUserFromDatabase(); }); // 获取用户信息后,调用外部服务进行处理 CompletableFuture<String> processedResult = userFuture.thenApplyAsync(user -> { // 模拟外部服务处理 return processUserData(user); }); // 处理完成后,执行其他操作 processedResult.thenAccept(result -> { System.out.println("处理结果: " + result); // 可以继续链式调用其他操作 }); // 等待异步操作完成并获取结果(示例,实际使用中应避免在异步编程中使用) try { String finalResult = processedResult.get(); System.out.println("最终结果: " + finalResult); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } } private static User getUserFromDatabase() { // 模拟数据库查询,返回用户信息 return new User("Vin", 30); } private static String processUserData(User user) { // 模拟外部服务处理用户数据 return "用户信息处理结果: " + user.getName() + ", 年龄: " + user.getAge(); } static class User { private String name; private int age; public User(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } } }
我们首先异步地从数据库获取用户信息,然后使用 thenApplyAsync
方法对用户信息进行处理,最后将处理结果输出到控制台。
这边整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记的【点击此处即可】免费获取
通过一个简单的例子,我们先感受了CompletableFuture类的功能,那CompletableFuture 在处理并发任务时有哪些优势和局限性呢?
先来看优势
-
链式调用:
CompletableFuture
提供了丰富的方法,如thenApply
、thenAccept
、thenRun
等,支持链式调用,使得异步操作的编排变得简洁。 -
支持Java 8的函数式编程: 它支持Java 8的函数式编程特性,可以使用 Lambda 表达式作为参数,提高代码的可读性和简洁性。
-
错误处理:
exceptionally
方法允许开发者为异步操作指定异常处理逻辑,使得错误处理更加集中和一致。 -
组合操作: 可以方便地组合多个
CompletableFuture
对象,例如使用allOf
或anyOf
来等待多个异步操作的完成。 -
非阻塞编程:
CompletableFuture
支持非阻塞编程,可以在等待异步操作结果时继续执行其他任务。
CompletableFuture
允许开发者自定义执行异步操作的线程池,提供了更好的控制力。并提供了多种API来处理异步操作,如 supplyAsync
、runAsync
等。相比传统的多线程同步方式,CompletableFuture
可以减少线程阻塞和上下文切换的开销。
再看局限性:
总结3点吧,在使用过程中需要注意一下:
-
过度使用可能导致性能问题: 如果过度使用
CompletableFuture
进行细粒度的异步操作,可能会导致过多的线程创建和上下文切换,反而降低性能。 -
阻塞调用: 虽然
CompletableFuture
支持非阻塞编程,但如果使用不当,例如使用get
方法等待结果,可能会阻塞当前线程,失去异步的优势。 -
错误的使用方式:
CompletableFuture
的一些方法,如thenAcceptBoth
、thenCombine
等,如果使用不当,可能会导致错误的逻辑处理。
CompletableFuture
是一个强大的工具,可以极大地简化异步编程的复杂性,但也需要开发者有良好的并发编程知识,以避免引入新的问题。
CompletableFuture 在处理并发任务时,如何避免资源泄露的问题?
避免资源泄露的问题
在Java中,资源泄露通常是指没有正确关闭那些占用系统资源的对象,如文件句柄、数据库连接、网络连接等。在使用CompletableFuture
时,如果异步操作中使用了这些资源,但没有在操作完成后正确关闭它们,就可能导致资源泄露。
下面这个示例,咱们来演示可能导致资源泄露的CompletableFuture
使用方式:
java
代码解读
复制代码
import java.io.Closeable; import java.io.IOException; import java.util.concurrent.CompletableFuture; class Resource implements Closeable { public Resource() { // 构造函数中初始化资源 } @Override public void close() throws IOException { // 清理和关闭资源 System.out.println("Resource closed"); } } public class ResourceLeakExample { public static void main(String[] args) { // 创建资源对象 Resource resource = new Resource(); // 使用CompletableFuture执行异步操作,但没有正确关闭资源 CompletableFuture<Void> future = CompletableFuture.runAsync(() -> { // 假设这里执行了一些操作 try { // 模拟长时间运行的任务 Thread.sleep(5000); System.out.println("Task completed"); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } // 没有调用resource.close(),导致资源泄露 }); // 没有等待异步操作完成并关闭资源 // future.join(); // 这行代码如果被注释掉,将不会等待异步任务完成 } }
在这个例子中,Resource
类代表了一个需要在使用后关闭的资源。在main
方法中,我们创建了一个Resource
对象,并在一个CompletableFuture
的异步任务中使用它。然而,这个任务完成后并没有调用resource.close()
来关闭资源。此外,main
方法也没有等待异步任务完成(即使调用了future.join()
,也不会调用close()
)。
这将导致以下问题:
Resource
对象没有被关闭,导致资源泄露。- 如果这个模式在整个应用程序中广泛存在,可能会导致系统资源耗尽。
为了避免资源泄露,我们应该确保在异步操作完成后关闭资源。这可以通过以下几种方式实现:
- 使用
try
-with-resources语句确保自动关闭资源。 - 在
CompletableFuture
的whenComplete
或exceptionally
方法中调用资源的关闭方法。 - 使用
finally
块来关闭资源。
那要怎么办呢,我们尝试来修改一下,如何在异步操作完成后正确关闭资源:
java
代码解读
复制代码
public class ProperResourceManagementExample { public static void main(String[] args) { Resource resource = new Resource(); CompletableFuture<Void> future = CompletableFuture.runAsync(() -> { try { // 使用资源执行任务 Thread.sleep(5000); System.out.println("Task completed"); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { try { resource.close(); } catch (IOException e) { e.printStackTrace(); } } }); // 等待异步操作完成 future.join(); } }
在这个修改后的示例中,我们在try
块中使用资源,并在finally
块中确保资源被关闭。这样即使发生异常,资源也会被正确关闭,从而避免了资源泄露。
小结一下
所以在使用 CompletableFuture 进行并发编程时,我们需要遵循一些实践来有效地避免资源泄露的问题: 在使用 CompletableFuture
进行并发编程时,遵循一些最佳实践可以有效地避免资源泄露:
-
使用 try-with-resources 语句: 确保在异步操作中使用的资源实现了
AutoCloseable
接口,并在try
-with-resources 语句中声明,以便它们可以自动关闭。 -
在 finallyApply 或 exceptionally 中关闭资源: 使用
finallyApply
方法来转换结果并在操作完成后关闭资源。如果操作失败,使用exceptionally
方法来处理异常并关闭资源。 -
确保异步任务完成: 使用
join
或get
方法等待异步任务完成,确保在应用程序关闭前所有任务都已完成。 -
避免资源在多处引用: 避免在异步操作中捕获外部资源的引用,这可能导致资源无法被垃圾收集器回收。
-
使用线程池: 自定义线程池时,确保线程池的大小适合应用程序的需求,并且正确管理线程池的生命周期。
-
合理配置线程池: 使用合理的线程池配置,例如设置最大线程数和队列容量,以避免资源耗尽。
-
监控资源使用情况: 使用适当的监控工具来跟踪资源使用情况,以便及时发现和解决资源泄露问题。
-
避免阻塞调用: 避免在异步操作链中使用阻塞调用,如
get
或join
,这可能会导致死锁或资源无法释放。 -
使用超时机制: 在调用
get
方法时,使用超时参数,以避免无限期地等待异步操作完成。 -
正确处理异常: 在异步操作链中使用
exceptionally
方法来捕获和处理异常,确保即使在发生异常时也能释放资源。 -
避免资源饥饿: 确保异步任务不会长时间占用资源,特别是在使用数据库连接或其他有限资源时。
-
使用资源池: 对于数据库连接、网络连接等资源,使用资源池来管理资源的分配和释放。
-
避免过度使用 CompletableFuture: 对于简单的同步操作,避免使用
CompletableFuture
,以减少不必要的复杂性和资源消耗。 -
使用 CompletableFuture 的组合方法: 利用
CompletableFuture
提供的组合方法,如thenCombine
、thenAcceptBoth
等,来避免创建不必要的中间对象。
以上这些,你可以在使用 CompletableFuture
进行并发编程时减少资源泄露的风险,并提高应用程序的稳定性和性能。