多线程彻底掌握 CompletableFuture:从入门到项目实战

目录

一、为什么需要 CompletableFuture

在 Java 并发编程中,最早我们通常使用 ThreadRunnableCallableFuture 来执行异步任务。

例如:

java 复制代码
ExecutorService executor = Executors.newFixedThreadPool(5);

Future<String> future = executor.submit(() -> {
    Thread.sleep(1000);
    return "Hello";
});

String result = future.get();

虽然可以实现异步执行,但存在明显缺点:

  • get() 会阻塞当前线程
  • 无法方便地进行任务编排
  • 无法链式调用
  • 多任务组合比较麻烦
  • 异常处理不够优雅

因此 JDK8 引入了 CompletableFuture

它不仅能够表示一个异步任务,还支持:

  • 异步回调
  • 链式调用
  • 多任务编排
  • 异常处理
  • 并发任务组合

在 Spring Boot、微服务、Feign 调用、批量处理等场景中使用非常广泛。


二、创建 CompletableFuture

1、runAsync(无返回值)

执行一个异步任务,但不返回结果。

java 复制代码
CompletableFuture.runAsync(() -> {
    System.out.println("执行异步任务");
});

源码:

java 复制代码
public static CompletableFuture<Void> runAsync(Runnable runnable)

返回值:

java 复制代码
CompletableFuture<Void>

2、supplyAsync(有返回值)

执行异步任务并返回结果。

java 复制代码
CompletableFuture<String> future =
        CompletableFuture.supplyAsync(() -> {
            return "Hello";
        });

获取结果:

java 复制代码
String result = future.join();

输出:

text 复制代码
Hello

3、指定线程池

生产环境不建议直接使用:

java 复制代码
CompletableFuture.runAsync(...)

因为默认使用:

java 复制代码
ForkJoinPool.commonPool()

推荐使用业务线程池:

java 复制代码
ExecutorService executor =
        Executors.newFixedThreadPool(10);

CompletableFuture.runAsync(() -> {
    System.out.println(Thread.currentThread().getName());
}, executor);

三、获取执行结果

1、get()

java 复制代码
String result = future.get();

特点:

  • 阻塞等待
  • 需要处理检查异常
java 复制代码
InterruptedException
ExecutionException

2、join()

java 复制代码
String result = future.join();

特点:

  • 阻塞等待
  • 不需要处理检查异常

项目中使用更频繁。


3、getNow()

立即返回结果。

java 复制代码
String result = future.getNow("默认值");

如果任务未完成:

text 复制代码
返回默认值

四、任务完成后的回调

1、thenAccept

消费结果但不返回值。

java 复制代码
CompletableFuture.supplyAsync(() -> "hello")
        .thenAccept(System.out::println);

输出:

text 复制代码
hello

2、thenRun

不关心前面任务结果。

java 复制代码
CompletableFuture.supplyAsync(() -> "hello")
        .thenRun(() -> {
            System.out.println("执行完成");
        });

3、thenApply

对结果进行转换。

java 复制代码
CompletableFuture<String> future =
        CompletableFuture.supplyAsync(() -> "hello")
                .thenApply(String::toUpperCase);

System.out.println(future.join());

输出:

text 复制代码
HELLO

执行流程:

text 复制代码
hello
 ↓
thenApply
 ↓
HELLO

五、任务编排

1、thenApply

用于结果转换。

java 复制代码
CompletableFuture<Integer> future =
        CompletableFuture.supplyAsync(() -> 10)
                .thenApply(i -> i * 2)
                .thenApply(i -> i + 5);

System.out.println(future.join());

结果:

text 复制代码
25

流程:

text 复制代码
10
 ↓
20
 ↓
25

2、thenCompose

用于依赖任务。

例如:

先查用户,再查订单。

java 复制代码
CompletableFuture<Order> future =
        CompletableFuture.supplyAsync(() -> queryUser(1L))
                .thenCompose(user ->
                        CompletableFuture.supplyAsync(
                                () -> queryOrder(user.getId())
                        ));

流程:

text 复制代码
查询用户
    ↓
获取用户ID
    ↓
查询订单

3、thenApply 与 thenCompose 的区别

错误写法:

java 复制代码
CompletableFuture<CompletableFuture<Order>>
.thenApply(user ->
        CompletableFuture.supplyAsync(
                () -> queryOrder(user.getId())
        ))

得到的是嵌套 Future。


正确写法:

java 复制代码
.thenCompose(...)

结果:

java 复制代码
CompletableFuture<Order>

六、多任务并发执行

这是实际项目最常见的场景。

例如订单详情页面:

需要同时查询:

  • 用户信息
  • 订单信息
  • 发票信息

如果顺序执行:

text 复制代码
1秒 + 1秒 + 1秒 = 3秒

如果并发执行:

text 复制代码
max(1秒,1秒,1秒)=1秒

1、allOf

等待全部任务完成。

java 复制代码
CompletableFuture<User> userFuture =
        CompletableFuture.supplyAsync(() -> queryUser());

CompletableFuture<Order> orderFuture =
        CompletableFuture.supplyAsync(() -> queryOrder());

CompletableFuture<Invoice> invoiceFuture =
        CompletableFuture.supplyAsync(() -> queryInvoice());

CompletableFuture.allOf(
        userFuture,
        orderFuture,
        invoiceFuture
).join();

此时:

text 复制代码
三个任务全部完成
       ↓
继续执行

2、anyOf

任意一个完成即可返回。

java 复制代码
CompletableFuture<Object> future =
        CompletableFuture.anyOf(
                future1,
                future2,
                future3
        );

适用于:

  • 多缓存节点
  • 多数据源
  • 多搜索服务

谁先返回用谁。


七、异常处理

1、exceptionally

发生异常时返回默认值。

java 复制代码
CompletableFuture<Integer> future =
        CompletableFuture.supplyAsync(() -> {
            int i = 1 / 0;
            return 100;
        })
        .exceptionally(ex -> -1);

System.out.println(future.join());

结果:

text 复制代码
-1

2、handle

无论成功还是失败都会执行。

java 复制代码
CompletableFuture<Integer> future =
        CompletableFuture.supplyAsync(() -> 100)
                .handle((result, ex) -> {

                    if (ex != null) {
                        return -1;
                    }

                    return result * 2;
                });

结果:

text 复制代码
200

3、whenComplete

常用于日志记录。

java 复制代码
CompletableFuture.supplyAsync(() -> 100)
        .whenComplete((result, ex) -> {

            System.out.println(result);

            if (ex != null) {
                ex.printStackTrace();
            }
        });

八、重点区别

Future 和 CompletableFuture 的区别

Future CompletableFuture
只能获取结果 支持回调
不支持编排 支持链式编排
不支持组合 支持 allOf
功能简单 功能强大

thenApply 和 thenCompose 区别

thenApply:

text 复制代码
A → B

结果转换。

thenCompose:

text 复制代码
A → CompletableFuture<B>

自动拍平 Future。


allOf 和 anyOf 区别

allOf:

text 复制代码
全部完成继续

anyOf:

text 复制代码
任意完成继续

为什么生产环境要使用自定义线程池

避免所有业务共享:

java 复制代码
ForkJoinPool.commonPool()

导致:

  • 线程耗尽
  • 请求堆积
  • 性能抖动

推荐:

java 复制代码
CompletableFuture.runAsync(
        task,
        customExecutor
);

九、项目实战:批量推送订单到财务系统

实战背景

假设系统需要向财务系统推送 100 个订单。

每个订单调用一次远程接口:

text 复制代码
推送订单1
推送订单2
推送订单3
...
推送订单100

每次推送耗时约 1 秒。

如果串行执行:

text 复制代码
100 × 1秒 = 100秒

用户需要等待很久。

因此我们希望:

text 复制代码
100个订单同时推送

利用线程池并发处理。


第一步:配置线程池

Spring Boot 配置:

java 复制代码
@Configuration
public class AsyncThreadPoolConfig {

    @Bean
    public ThreadPoolTaskExecutor asyncThreadPool() {

        ThreadPoolTaskExecutor executor =
                new ThreadPoolTaskExecutor();

        // 核心线程数
        executor.setCorePoolSize(5);

        // 最大线程数
        executor.setMaxPoolSize(10);

        // 队列容量
        executor.setQueueCapacity(100);

        // 线程名前缀
        executor.setThreadNamePrefix("push-order-");

        // 优雅关闭
        executor.setWaitForTasksToCompleteOnShutdown(true);

        executor.initialize();

        return executor;
    }
}

第二步:编写业务代码

可以直接复制运行观察效果。

java 复制代码
@Service
public class PushOrderService {

    @Autowired
    private ThreadPoolTaskExecutor asyncThreadPool;

    public void pushOrders() {

        // 模拟100个订单
        List<String> orders = IntStream.rangeClosed(1, 100)
                .mapToObj(i -> "ORDER-" + i)
                .collect(Collectors.toList());

        // 用于保存所有异步任务
        List<CompletableFuture<Void>> futures = new ArrayList<>();
        long start = System.currentTimeMillis();
        //通过循环 把100个订单都放进线程池 ,然后放到 futures 里
        for (String orderNo : orders) {
            CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + " 开始推送:" + orderNo);

                    // 模拟远程调用耗时
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() + " 推送成功:" + orderNo);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, asyncThreadPool);

            futures.add(future);
        }

        //把所有Future合并成一个Future,然后等待全部订单推送完成
        CompletableFuture.allOf(
                futures.toArray(new CompletableFuture[0])
        ).join();

        long end = System.currentTimeMillis();

        System.out.println("总耗时:" + (end - start) + " ms");
    }
}

第三步:理解代码执行流程

假设线程池:

text 复制代码
核心线程数:5

订单:

text 复制代码
ORDER-1
ORDER-2
...
ORDER-100

执行流程:

text 复制代码
线程1 -> ORDER-1
线程2 -> ORDER-2
线程3 -> ORDER-3
线程4 -> ORDER-4
线程5 -> ORDER-5

第一批完成:

text 复制代码
线程1 -> ORDER-6
线程2 -> ORDER-7
...

持续执行。


第四步:allOf 在这里做了什么

核心代码:

java 复制代码
CompletableFuture.allOf(
        futures.toArray(new CompletableFuture[0])
).join();

作用:

text 复制代码
等待所有订单推送完成

流程:

text 复制代码
future1 ────────┐
future2 ────────┤
future3 ────────┤
future4 ────────┤
future5 ────────┘
         ↓
      allOf
         ↓
      join
         ↓
继续执行

如果少了这段代码:

java 复制代码
CompletableFuture.runAsync(...)

提交后方法会立刻结束。

此时:

text 复制代码
订单还没推送完
方法已经返回

而有了:

java 复制代码
allOf(...).join()

系统会等待所有订单推送成功后再继续执行后面的逻辑。


上面的 CompletableFuture 实战在企业项目中也常使用于以下场景:

text 复制代码
批量推送
批量导入
批量同步
批量调用Feign
批量调用第三方接口
相关推荐
左左右右左右摇晃1 小时前
手写Tomcat原理整理
java·开发语言·笔记·tomcat
孫治AllenSun1 小时前
【JVM】四大引用类型分析
java·开发语言·jvm
于慨1 小时前
安装java
java
夕除1 小时前
sign 是什么
java·前端
Penina10181 小时前
代码如何导入idea
java·intellij idea
.Hypocritical.1 小时前
Maven笔记——2
java·笔记·maven
huohuopro2 小时前
手写 Tomcat 步骤教程
java·网络
snow@li2 小时前
Java:Lombok 完整讲解
java
Shadow(⊙o⊙)2 小时前
高并发内存池:Part-1——定长内存池
java·前端·javascript
国服第二切图仔2 小时前
17-config命令 - 配置管理系统
java·前端·javascript