Java并发包中的CompletableFuture类使用详解

569 阅读8分钟

CompletableFuture 是 Java 8 引入的一个非常强大的类,它提供了一个可扩展的异步编程模型。CompletableFuture 表示一个可能还没有完成的异步计算,也就是说可以异步地执行任务,不阻塞当前线程,它提供了多种方法来组合和转换结果。

CompletableFuture 使用 thenApplythenAcceptthenRun 等方法,可以在计算完成时指定回调函数。

CompletableFuture 可以组合其他 CompletableFuture 对象,例如使用 thenCompose 方法。使用 exceptionally 方法可以处理异步操作中的异常。

CompletableFuture 的方法通常返回 CompletableFuture 类型,支持链式调用。

CompletableFuture 默认使用 ForkJoinPool 线程池,但可以通过 Executor 自定义线程池。

CompletableFuture 有多种完成阶段,如 completedfailedcancelled

如果想要执行同步等待操作,使用 get 方法可以等待异步操作完成,但要注意的是,这可能引发的 InterruptedException异常。

以上是CompletableFuture的关键特性,下面我们来看一个业务场景案例,假设我们有一个场景,需要从数据库获取用户信息,然后根据用户信息调用外部服务进行处理,最后返回处理结果。

代码案例:

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 方法对用户信息进行处理,最后将处理结果输出到控制台。

通过一个简单的例子,我们先感受了CompletableFuture类的功能,那CompletableFuture 在处理并发任务时有哪些优势和局限性呢?

先来看优势

  1. 链式调用CompletableFuture 提供了丰富的方法,如 thenApplythenAcceptthenRun 等,支持链式调用,使得异步操作的编排变得简洁。

  2. 支持Java 8的函数式编程: 它支持Java 8的函数式编程特性,可以使用 Lambda 表达式作为参数,提高代码的可读性和简洁性。

  3. 错误处理exceptionally 方法允许开发者为异步操作指定异常处理逻辑,使得错误处理更加集中和一致。

  4. 组合操作: 可以方便地组合多个 CompletableFuture 对象,例如使用 allOfanyOf 来等待多个异步操作的完成。

  5. 非阻塞编程CompletableFuture 支持非阻塞编程,可以在等待异步操作结果时继续执行其他任务。

CompletableFuture允许开发者自定义执行异步操作的线程池,提供了更好的控制力。并提供了多种API来处理异步操作,如 supplyAsyncrunAsync 等。相比传统的多线程同步方式,CompletableFuture 可以减少线程阻塞和上下文切换的开销。

再看局限性:

总结3点吧,在使用过程中需要注意一下:

  1. 过度使用可能导致性能问题: 如果过度使用 CompletableFuture 进行细粒度的异步操作,可能会导致过多的线程创建和上下文切换,反而降低性能。

  2. 阻塞调用: 虽然 CompletableFuture 支持非阻塞编程,但如果使用不当,例如使用 get 方法等待结果,可能会阻塞当前线程,失去异步的优势。

  3. 错误的使用方式CompletableFuture 的一些方法,如 thenAcceptBoththenCombine 等,如果使用不当,可能会导致错误的逻辑处理。

CompletableFuture 是一个强大的工具,可以极大地简化异步编程的复杂性,但也需要开发者有良好的并发编程知识,以避免引入新的问题。

CompletableFuture 在处理并发任务时,如何避免资源泄露的问题?

避免资源泄露的问题

在Java中,资源泄露通常是指没有正确关闭那些占用系统资源的对象,如文件句柄、数据库连接、网络连接等。在使用CompletableFuture时,如果异步操作中使用了这些资源,但没有在操作完成后正确关闭它们,就可能导致资源泄露。

下面这个示例,咱们来演示可能导致资源泄露的CompletableFuture使用方式:

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语句确保自动关闭资源。
  • CompletableFuturewhenCompleteexceptionally方法中调用资源的关闭方法。
  • 使用finally块来关闭资源。

那要怎么办呢,我们尝试来修改一下,如何在异步操作完成后正确关闭资源:

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 进行并发编程时,遵循一些最佳实践可以有效地避免资源泄露:

  1. 使用 try-with-resources 语句: 确保在异步操作中使用的资源实现了 AutoCloseable 接口,并在 try-with-resources 语句中声明,以便它们可以自动关闭。

  2. 在 finallyApply 或 exceptionally 中关闭资源: 使用 finallyApply 方法来转换结果并在操作完成后关闭资源。如果操作失败,使用 exceptionally 方法来处理异常并关闭资源。

  3. 确保异步任务完成: 使用 joinget 方法等待异步任务完成,确保在应用程序关闭前所有任务都已完成。

  4. 避免资源在多处引用: 避免在异步操作中捕获外部资源的引用,这可能导致资源无法被垃圾收集器回收。

  5. 使用线程池: 自定义线程池时,确保线程池的大小适合应用程序的需求,并且正确管理线程池的生命周期。

  6. 合理配置线程池: 使用合理的线程池配置,例如设置最大线程数和队列容量,以避免资源耗尽。

  7. 监控资源使用情况: 使用适当的监控工具来跟踪资源使用情况,以便及时发现和解决资源泄露问题。

  8. 避免阻塞调用: 避免在异步操作链中使用阻塞调用,如 getjoin,这可能会导致死锁或资源无法释放。

  9. 使用超时机制: 在调用 get 方法时,使用超时参数,以避免无限期地等待异步操作完成。

  10. 正确处理异常: 在异步操作链中使用 exceptionally 方法来捕获和处理异常,确保即使在发生异常时也能释放资源。

  11. 避免资源饥饿: 确保异步任务不会长时间占用资源,特别是在使用数据库连接或其他有限资源时。

  12. 使用资源池: 对于数据库连接、网络连接等资源,使用资源池来管理资源的分配和释放。

  13. 避免过度使用 CompletableFuture: 对于简单的同步操作,避免使用 CompletableFuture,以减少不必要的复杂性和资源消耗。

  14. 使用 CompletableFuture 的组合方法: 利用 CompletableFuture 提供的组合方法,如 thenCombinethenAcceptBoth 等,来避免创建不必要的中间对象。

以上这些,你可以在使用 CompletableFuture 进行并发编程时减少资源泄露的风险,并提高应用程序的稳定性和性能。