前言
大家好,我是田螺。
日常开发中,我们经常喜欢用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();
正例:
使用CompletableFuture
的supplyAsync
或 runAsync
时,手动传递上下文。
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(); // 确保顺序执行