【Java高手进阶】CompletableFuture从入门到精通,这篇全讲透了!

图片

并发 (Concurrency)

并发指的是程序能够同时处理多个任务,或者通过在这些任务之间快速切换来管理它们的能力。这在处理需要同时应对多个用户、任务或事件的应用程序中尤其重要。

在 Java 中,并发主要依赖于线程 (threads)
线程是 Java 应用程序中最小的独立执行单元。
Java 透过同时运行多个线程来支持并发,每个线程都独立于其他线程执行。

示例: 创建并运行一个基础的 Java 线程:

class MyTask implements Runnable {
    @Override
    public void run() {
        // 打印当前线程的名称
        System.out.println("正在并发运行任务: " + Thread.currentThread().getName());
    }
}

public class ThreadExample {
    public static void main(String[] args) {
        // 创建两个任务
        Thread thread1 = new Thread(new MyTask());
        Thread thread2 = new Thread(new MyTask());

        // 启动线程
        thread1.start();
        thread2.start();

        // 主线程继续执行
        System.out.println("主线程: " + Thread.currentThread().getName());
    }
}

输出可能会像这样(顺序不保证):

主线程: main
正在并发运行任务: Thread-0
正在并发运行任务: Thread-1

并行 (Parallelism)

并行指的是真正地在同一时刻执行多个任务。具体来说,它意味着利用多个 CPU 核心或处理器,在物理上同时运行多个任务或指令。

与并发不同,并行不仅仅是管理多个任务——它是为了提升性能而明确地在同一瞬间执行多个任务。

示例: 想象一下对一个巨大的数据集进行排序。通过并行处理,你可以将数据集分割成多个小块,然后在多个 CPU 核心上同时对每个小块进行排序。

核心区别:

  • • 并发 (Concurrency) 是关于通过交错执行处理多个任务,允许你同时管理许多任务(但不一定是在完全相同的瞬间执行)。

  • • 并行 (Parallelism) 则涉及在完全相同的时刻执行多个任务(通常需要多个 CPU 核心)。

餐厅厨房的比喻

想象一个餐厅厨房:

  • • 1 个厨师 = 1 个 CPU 核心

  • • 4 个厨师 = 4 个 CPU 核心

  • • 一长串的菜单订单 = 需要执行的任务

并发 (1 个厨师,很多道菜)
一个厨师通过在不同菜品之间来回切换来同时处理多道菜。
他可能先开始煮意面,然后在意面煮着的时候去切蔬菜,然后再回到意面那边。
在任何一个瞬间,他只在做一件事情,但因为他在任务间交错进行,所以看起来像是在同时做很多事。
这就是并发

并行 (4 个厨师,同时做 4 道菜)
每个厨师在同一时间各自做一道菜。
一个厨师做意面,另一个做牛排,第三个做沙拉,第四个做汤。
所有人都在同一时刻工作。
这就是并行

现在,用 Java 的术语来说:

// 创建一个包含4个线程的线程池
ExecutorService pool = Executors.newFixedThreadPool(4);

这意味着:

  • • 你创建了一个有 **4 个线程(厨师)**的池子。

  • • 如果你的电脑有 4 个或更多 CPU 核心,那么这 4 个线程就可以真正地并行运行 → 这是并行 ✅

  • • 如果你的电脑只有 1 个核心,那么这 4 个线程将轮流交错执行 → 这只是并发 ✅


ThreadPool, ExecutorService 和 Future

手动管理线程很快就会变得复杂且容易出错。为了简化并发编程并提高效率,Java 提供了强大的工具,如线程池 (ThreadPools)ExecutorService 框架以及 Future 接口。

线程池 (ThreadPool): 高效的线程管理

为每个新任务都创建一个新线程的成本是很高的——会消耗大量的内存和 CPU 开销。线程池通过维护一个可复用的线程集合来解决这个问题,这些线程可以随时用来执行多个任务。

  • • 线程池的好处:

    • • 减少了因创建和销毁线程而产生的开销。

    • • 通过复用线程来限制资源消耗。

    • • 通过将任务放入队列来高效地管理工作负载。

ExecutorService: 灵活的线程池管理器

ExecutorService 是 Java 对线程池管理器的实现。它通过自动处理线程的创建、复用和调度,极大地简化了线程管理。

  • • 创建一个 ExecutorService
    Java 提供了内置的静态工厂方法来轻松创建不同类型的线程池:

    • • 固定大小的线程池:
      ExecutorService executor = Executors.newFixedThreadPool(10);
    • • 缓存线程池: 根据工作负载动态调整线程数量。
      ExecutorService executor = Executors.newCachedThreadPool();
    • • 定时任务线程池: 在给定延迟后或周期性地执行任务。
      ScheduledExecutorService executor = Executors.newScheduledThreadPool(5);
    • • 单线程执行器: 确保任务按顺序依次执行。
      ExecutorService executor = Executors.newSingleThreadExecutor();

动手实践:从阻塞式调用到异步编程

在本节中,我们将通过一个动手示例,来探讨 Java 中默认的同步阻塞式调用是如何工作的,以及如何用 Future 和 CompletableFuture 将其改进为异步非阻塞。我们将使用两个简单的服务:一个用于获取电影,另一个用于获取评分。

示例代码 (初始状态):

// Movie.java - 代表电影对象
public class Movie {
    private String title;
    private Rating rating;
    // 构造函数、getter 和 setter 省略...
}

// Rating.java - 代表电影评分
public class Rating {
    private int movieId;
    private int stars;
    // 构造函数、getter 和 setter 省略...
}

// MovieService.java - 模拟获取电影(带有人为延迟)
public class MovieService {
    public static Movie getMovie() {
        Utils.delay(); // 模拟慢服务
        return new Movie("一部情感电影", null);
    }
}

// RatingService.java - 模拟获取评分(带有人为延迟)
public class RatingService {
    public static Rating getRating() {
        Utils.delay(); // 模拟慢服务
        return new Rating(5, 5);
    }
}

// Utils.java - 包含一个模拟延迟的静态方法
public class Utils {
    public static void delay() {
        try {
            Thread.sleep(1000); // 延迟1秒
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt(); // 恢复中断状态
            System.err.println("线程被中断: " + e.getMessage());
        }
    }
}

// 主程序 - 阻塞式调用
public class BlockingExample {
    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        
        // 1. 调用 MovieService,主线程阻塞等待1秒
        Movie movie = MovieService.getMovie();
        System.out.println("获取到电影...");

        // 2. 调用 RatingService,主线程再次阻塞等待1秒
        Rating rating = RatingService.getRating();
        System.out.println("获取到评分...");

        movie.setRating(rating);
        System.out.println("电影: " + movie.getTitle() + ", 评分: " + movie.getRating().getStars());
        
        long endTime = System.currentTimeMillis();
        System.out.println("总耗时: " + (endTime - startTime) + " 毫秒"); // 大约 2000 毫秒
    }
}

在上面的阻塞式示例中,获取电影和获取评分是串行执行的,总耗时是两者之和(约2秒)。


Future

在 Java 5 中引入的 Future 接口是向异步编程迈出的重要一步——它允许使用 ExecutorService 异步地执行任务。然而,Future 有其显著的局限性:它的 get() 方法本质上是阻塞的,它缺乏对任务组合的内置支持,并且不容易实现回调驱动的编程。

通过 Future,我们可以将任务提交到线程池中,让获取电影和获取评分这两个服务调用并发执行。然后,我们使用 future.get() 等待两个结果。

import java.util.concurrent.*;

public class FutureExample {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        long startTime = System.currentTimeMillis();

        // 并行提交两个任务
        Future<Movie> movieFuture = executor.submit(MovieService::getMovie);
        Future<Rating> ratingFuture = executor.submit(RatingService::getRating);
        System.out.println("任务已提交...");

        // 等待两个结果(仅当结果尚未就绪时才会阻塞)
        Movie movie = movieFuture.get();     // 如果任务仍在运行,这里会等待
        System.out.println("获取到电影 (耗时约1秒)...");
        Rating rating = ratingFuture.get();  // 如果任务仍在运行,这里会等待
        System.out.println("获取到评分 (总耗时仍约1秒)...");

        movie.setRating(rating);
        System.out.println("电影: " + movie.getTitle() + ", 评分: " + movie.getRating().getStars());

        executor.shutdown();
        long endTime = System.currentTimeMillis();
        System.out.println("总耗时: " + (endTime - startTime) + " 毫秒"); // 大约 1000 毫秒
    }
}

submit() 方法在线程池的独立线程中异步运行这两个任务。
movieFuture.get() 和 ratingFuture.get() 仅在结果尚未就绪时才会阻塞。
因为这两个耗时1秒的任务是并行运行的,所以总执行时间从大约2秒减少到了大约1秒。


CompletableFuture

在 Java 8 中引入的 CompletableFuture 是一个功能极其强大的抽象,它允许开发者以一种干净、流式的方式处理异步计算。

其核心思想是,CompletableFuture 代表一个承诺 (promise):一个可能在未来某个时间点才会产生的结果。但与旧的 Future 不同,它提供了一套丰富的 API,使得我们能够:

  • • 异步运行任务。

  • • 链式调用和组合多个异步操作。

  • • 在结果就绪时对其应用转换

  • • 以声明式的风格处理异常。

  • • 所有这些操作都无需阻塞主线程。

这种模型非常适合诸如调用外部 API、并行处理数据、协调多个相互依赖的任务,或构建非阻塞微服务等场景。

import java.util.concurrent.*;

public class CompletableFutureExample {
    public static void main(String[] args) throws InterruptedException {
        long startTime = System.currentTimeMillis();

        // 异步获取电影
        CompletableFuture<Movie> movieFuture = CompletableFuture.supplyAsync(MovieService::getMovie);
        // 异步获取评分
        CompletableFuture<Rating> ratingFuture = CompletableFuture.supplyAsync(RatingService::getRating);
        System.out.println("异步任务已启动...");

        // 定义当两个 Future 都完成时要执行的操作
        movieFuture
            .thenCombine(ratingFuture, (movie, rating) -> { // 组合结果
                movie.setRating(rating);
                return movie;
            })
            .thenAccept(movie -> { // 消费最终结果
                System.out.println("🎬 电影: " + movie.getTitle() + ", ⭐ 评分: " + movie.getRating().getStars());
                long endTime = System.currentTimeMillis();
                System.out.println("总耗时: " + (endTime - startTime) + " 毫秒"); // 大约 1000 毫秒
            })
            .exceptionally(ex -> { // 统一处理任何步骤中发生的异常
                System.out.println("❌ 发生错误: " + ex.getMessage());
                return null; // exceptionally 需要一个返回值
            });

        System.out.println("主线程可以继续做其他事情...");
        Thread.sleep(2000); // 阻塞主线程以等待异步任务完成,以便观察输出
    }
}
  • • .supplyAsync(Supplier<U> supplier) — 启动一个异步任务
    此方法异步地运行一个 Supplier(提供者),默认使用公共的 ForkJoinPool 线程池。

  • • .thenCombine(CompletionStage<V> other, BiFunction<T,V,U> fn) — 当两个 Future 都完成时,组合它们的结果
    当你需要等待两个独立的异步任务都完成后,再将它们的结果合并处理时,使用此方法。

  • • .thenApply(Function<T,U> fn) — 转换结果
    一旦你的异步结果就绪,你可以对其进行转换(比如映射数据)。

  • • .exceptionally(Function<Throwable,? extends T> fn) — 优雅地处理错误
    如果异步流程中任何地方出错了(例如,API 调用失败),你可以提供一个回退逻辑。

  • • .thenAccept(Consumer<T> action) — 消费结果(无返回值)
    当所有操作都完成,并且你对最终结果感到满意时,用此方法来消费结果。


其他有用的 CompletableFuture 方法
  1. 1. .thenRun(Runnable action)
    在前一个阶段完成后运行一个 Runnable 任务,但它不消费结果,也不产生新结果。
    CompletableFuture
        .supplyAsync(MovieService::getMovie)
        .thenRun(() -> System.out.println("✅ 电影获取任务已完成!"));
    用例: 记录日志、发送指标,或执行不依赖于前一阶段结果的副作用操作。
  2. 2. .thenCompose(Function<? super T,? extends CompletionStage<U>> fn)
    将一个 Future 的结果“扁平化地”链接到另一个 Future。当第二个异步操作依赖于第一个异步操作的结果时,这个方法非常有用。
    // 异步获取电影,然后根据电影ID异步获取评分
    // CompletableFuture<Movie> movieWithRating = CompletableFuture
    //     .supplyAsync(MovieService::getMovie) // 1. 获取电影
    //     .thenCompose(movie -> // 2. movie 是第一步的结果
    //         CompletableFuture.supplyAsync(() -> RatingService.getRatingForMovie(movie.getId())) // 3. 启动第二个异步任务
    //             .thenApply(rating -> { // 4. 拿到评分后
    //                 movie.setRating(rating); // 5. 设置评分
    //                 return movie; // 6. 返回最终带评分的电影
    //             }));
    用例: 当你需要链式地调用相互依赖的异步操作时。
  3. 3. .handle((result, exception) -> {...})
    在一个地方同时处理正常结果和异常。
    CompletableFuture<Movie> future = CompletableFuture
        .supplyAsync(MovieService::getMovie)
        .handle((movie, ex) -> { // movie 是结果,ex 是异常
            if (ex != null) { // 如果发生了异常
                System.out.println("发生错误: " + ex.getMessage());
                return new Movie("不可用", new Rating(0, 0)); // 返回一个默认的电影对象
            }
            return movie; // 如果没有异常,正常返回电影对象
        });
    用例: 统一的成功和错误处理逻辑。
  4. 4. .complete(T value)
    手动完成一个 CompletableFuture,并设置其结果。
    CompletableFuture<String> future = new CompletableFuture<>();
    // ... 在某个时刻,从另一个线程或回调中
    future.complete("手动完成的结果");
    用例: 在与一些老旧的、基于回调的 API 集成或在测试场景中非常有用。
  5. 5. .orTimeout(long timeout, TimeUnit unit) (Java 9+)
    如果 Future 在指定的超时时间内没有完成,则会使其失败并抛出 TimeoutException
    // CompletableFuture<String> future = CompletableFuture
    //     .supplyAsync(() -> {
    //         Utils.delay(2000); // 模拟一个2秒的延迟
    //         return "Hello";
    //     })
    //     .orTimeout(1, TimeUnit.SECONDS); // 设置1秒超时,这将会失败
  6. 6. .completeOnTimeout(T value, long timeout, TimeUnit unit) (Java 9+)
    如果任务在指定的超时时间内没有完成,则使用一个备用值来完成 Future。
    // CompletableFuture<String> future = CompletableFuture
    //     .supplyAsync(() -> {
    //         Utils.delay(2000); // 模拟2秒延迟
    //         return "真实响应";
    //     })
    //     .completeOnTimeout("默认响应", 1, TimeUnit.SECONDS); // 1秒后超时,会返回 "默认响应"
    在高可用性系统中,当“某个响应”比“没有响应”更好的时候,这个方法很有用。
  7. 7. allOf(...) & anyOf(...)

    • • CompletableFuture.allOf(...)
      等待所有给定的 Future 都完成。
      // CompletableFuture<Void> all = CompletableFuture.allOf(movieFuture, ratingFuture, anotherFuture);
    • • CompletableFuture.anyOf(...)
      当给定的 Future 中任何一个完成时,就会返回(“先到先得”)。
      // CompletableFuture<Object> any = CompletableFuture.anyOf(futureFromSource1, futureFromSource2);
    • • allOf: 当你需要所有结果都准备好之后才能继续时使用。

    • • anyOf: 当你只需要最快返回的那个结果,并且可以忽略其他结果时使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

java干货

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值