CompletableFuture使用的6个坑

前言

大家好,我是田螺

日常开发中,我们经常喜欢用CompletableFuture。但是它在使用的过程中,容易忽略几个坑,今天田螺哥给大家盘点一下~~

  • 公众号捡田螺的小男孩 (有田螺精心原创的面试PDF)
  • github地址,感谢每颗star:github

CompletableFuture使用的优点

既然上来说CompletableFuture可能隐藏几个坑,那为什么我们还要使用它呢?

CompletableFuture 是 Java 8 引入的异步编程工具,它的核心优势在于简化异步任务编排、提升代码可读性和灵活性

我们来看一个使用CompletableFuture的例子吧,代码如下:

假设我们有两个任务服务,一个查询用户基本信息,一个是查询用户勋章信息。

ini 复制代码
public class FutureTest {

    public static void main(String[] args) throws InterruptedException, ExecutionException, TimeoutException {

        UserInfoService userInfoService = new UserInfoService();
        MedalService medalService = new MedalService();
        long userId =666L;
        long startTime = System.currentTimeMillis();

        //调用用户服务获取用户基本信息
        CompletableFuture<UserInfo> completableUserInfoFuture = CompletableFuture.supplyAsync(() -> userInfoService.getUserInfo(userId));

        Thread.sleep(300); //模拟主线程其它操作耗时

        CompletableFuture<MedalInfo> completableMedalInfoFuture = CompletableFuture.supplyAsync(() -> medalService.getMedalInfo(userId)); 

        UserInfo userInfo = completableUserInfoFuture.get(2,TimeUnit.SECONDS);//获取个人信息结果
        MedalInfo medalInfo = completableMedalInfoFuture.get();//获取勋章信息结果
        System.out.println("总共用时" + (System.currentTimeMillis() - startTime) + "ms");

    }
}

接下来,我们通过代码demo,阐述一下CompletableFuture使用的几个坑~

1.默认线程池的坑

CompletableFuture默认使用ForkJoinPool.commonPool() 作为线程池。如果任务阻塞或执行时间过长,可能会导致线程池耗尽,影响其他任务的执行。

反例:

ini 复制代码
  CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
            // 模拟长时间任务
            try {
                Thread.sleep(10000);
                System.out.println("捡田螺的小男孩666");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        future.join();

正例:

ini 复制代码
// 1. 手动创建线程池(核心参数可配置化)
int corePoolSize = 10;     // 核心线程数
int maxPoolSize = 10;      // 最大线程数(固定大小)
long keepAliveTime = 0L;   // 非核心线程空闲存活时间(固定线程池可设为0)
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100); // 有界队列(容量100)
RejectedExecutionHandler rejectionHandler = new ThreadPoolExecutor.AbortPolicy(); // 拒绝策略

ExecutorService customExecutor = new ThreadPoolExecutor(
    corePoolSize,
    maxPoolSize,
    keepAliveTime,
    TimeUnit.MILLISECONDS,
    workQueue,
    rejectionHandler
);

// 2. 提交异步任务
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
    try {
        Thread.sleep(10000); // 模拟耗时任务
        System.out.println("捡田螺的小男孩666");
        System.out.println("Task completed");
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}, customExecutor);

// 3. 阻塞等待任务完成
future.join();

2. 异常处理的坑

如果CompletableFuture 中的任务抛出异常,跟我们使用的传统try...catch有点不一样的

正例:

使用 exceptionally 或 handle 方法来处理异常。

arduino 复制代码
 CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            throw new RuntimeException("田螺测试异常!");
        });
        future.exceptionally(ex -> {
            System.err.println("异常: " + ex.getMessage());
            return -1; // 返回默认值
        }).join();

运行结果:

makefile 复制代码
异常: java.lang.RuntimeException: 田螺测试异常!

3. 超时处理的坑

CompletableFuture 本身不支持超时处理,如果任务长时间不完成,可能会导致程序一直等待

反例:

ini 复制代码
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    try {
        Thread.sleep(10000); //模拟任务耗时
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return 1;
});
future.join(); // 程序会一直等待

正例:

如果你是JDK8,使用 get() 方法并捕获 TimeoutException

csharp 复制代码
 CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return 1;
        });
        future.join(); // 程序会一直等待

        try {
            Integer result = future.get(3, TimeUnit.SECONDS); // 设置超时时间为1秒
            System.out.println("田螺等待3秒之后:"+result);
        } catch (TimeoutException e) {
            System.out.println("Task timed out");
            future.cancel(true); // 取消任务
        } catch (Exception e) {
            e.printStackTrace();
        }

如果你是Java 9 或更高版本,可以直接使用 orTimeout 和 completeOnTimeout 方法:

ini 复制代码
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    try {
        Thread.sleep(10000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return 1;
}).orTimeout(3, TimeUnit.SECONDS); // 3秒超时
future.exceptionally(ex -> {
    System.err.println("Timeout: " + ex.getMessage());
    return -1;
}).join();

4. 线程上下文传递的坑

CompletableFuture 默认不会传递线程上下文(如 ThreadLocal),这可能导致上下文丢失~~

csharp 复制代码
 ThreadLocal<String> threadLocal = new ThreadLocal<>();
        threadLocal.set("田螺主线程");
        CompletableFuture.runAsync(() -> {
            System.out.println(threadLocal.get()); // 输出 null
        }).join();

正例:

使用CompletableFuturesupplyAsyncrunAsync时,手动传递上下文。

ini 复制代码
 ThreadLocal<String> threadLocal = new ThreadLocal<>();
        threadLocal.set("田螺主线程");
        ExecutorService executor = Executors.newFixedThreadPool(1);
        CompletableFuture.runAsync(() -> {
            threadLocal.set("田螺子线程");
            System.out.println(threadLocal.get()); // 输出田螺子线程
        }, executor).join();

5. 回调地狱的坑

CompletableFuture 的回调地狱指的是在异步编程中,过度依赖回调方法(如 thenApply、thenAccept 等)导致代码嵌套过深、难以维护的现象。

当多个异步任务需要顺序执行或依赖前一个任务的结果时,如果直接嵌套回调,代码会变得臃肿且难以阅读。反例如下:

sql 复制代码
CompletableFuture.supplyAsync(() -> 1)
    .thenApply(result -> {
        System.out.println("Step 1: " + result);
        return result + 1;
    })
    .thenApply(result -> {
        System.out.println("Step 2: " + result);
        return result + 1;
    })
    .thenAccept(result -> {
        System.out.println("Step 3: " + result);
    });

正例

通过链式调用和方法拆分,保持代码简洁:

csharp 复制代码
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 1)
    .thenApply(this::step1)
    .thenApply(this::step2);

future.thenAccept(this::step3);

// 拆分逻辑到单独方法
private int step1(int result) {
    System.out.println("Step 1: " + result);
    return result + 1;
}

private int step2(int result) {
    System.out.println("Step 2: " + result);
    return result + 1;
}

private void step3(int result) {
    System.out.println("Step 3: " + result);
}

6. 任务编排,执行顺序混乱的坑

任务编排时,如果任务之间有依赖关系,可能会导致任务无法按预期顺序执行。

反例

ini 复制代码
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> 1);
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> 2);
CompletableFuture<Integer> result = future1.thenCombine(future2, (a, b) -> a + b);
result.join(); // 可能不会按预期顺序执行

正例:

使用 thenCompose 或 thenApply 来确保任务顺序。

ini 复制代码
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> 1);
CompletableFuture<Integer> future2 = future1.thenApply(a -> a + 2);
future2.join(); // 确保顺序执行
相关推荐
白衣神棍4 分钟前
【八股文】ArrayList和LinkedList的区别
java
啥都想学的又啥都不会的研究生8 分钟前
Redis设计与实现-数据持久化
java·数据库·redis·笔记·缓存·面试
uhakadotcom24 分钟前
Istio 服务网格:连接、保护和优化微服务的利器
后端·面试·github
Asthenia04121 小时前
Spring事务分析:@Transactional用久了,是不是忘了编程式事务了?
后端
王网aaa1 小时前
堆结构和堆排序
java·算法·排序算法
无名指的等待7121 小时前
SpringBoot实现一个Redis限流注解
spring boot·redis·后端
张志翔的博客2 小时前
RK3588 openssl-3.4.1 编译安装
开发语言·后端·scala
计算机-秋大田2 小时前
基于Spring Boot的小区疫情购物系统的设计与实现(LW+源码+讲解)
java·vue.js·spring boot·后端·课程设计
loveking62 小时前
SpringBoot调用华为云短信实现发短信功能
java·spring boot·华为云
李长渊哦2 小时前
Spring Boot 约定大于配置:实现自定义配置
java·spring boot·后端