文章目录
📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌
📙 作者: 编程技术圈(哇哥面试陪跑)
👉 欢迎关注、分享、评论
✔️ 持续分享更多干货内容
🌐🌏🌎➕tcmeta, 欢迎沟通交流
📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌
零、引入
“异步接口又雪崩了!线程池堵满,CPU 干到 100%,用户全在投诉!” 王二拍着桌子哀嚎 —— 他用 CompletableFuture 写了批量商品查询接口,没配置线程池,结果高峰期线程数暴涨到几千,服务器直接卡死。
哇哥端着保温杯过来,扫了眼代码冷笑:“你这是把 CompletableFuture 当‘甩手掌柜’,默认线程池是个坑,今天我教你线程池优化 + 超时控制,再给你一套实战模板,下次再崩我把你保温杯扔了!”
点赞 + 关注,跟着哇哥和王二,吃透 CompletableFuture 实战优化技巧,批量异步代码直接抄,生产环境稳如老狗!

一、王二的致命坑:默认线程池是 “隐形炸弹”

王二的批量查询接口逻辑很简单:同时查询 100 个商品的信息,用 CompletableFuture 并行处理,代码看着没问题,一压测就崩。
👉 王二的 “裸奔” 代码(无自定义线程池)
package cn.tcmeta.completables;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
/**
* @author: laoren
* @description: 王二的坑:用CompletableFuture默认线程池,高并发下崩了
* @version: 1.0.0
*/
public class CompletableFutureThreadPoolDisasterSample {
// 模拟查询单个商品信息(耗时50ms)
private static CompletableFuture<String> getProductInfo(String productId) {
// 坑:用默认线程池ForkJoinPool.commonPool()
return CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.MILLISECONDS.sleep(50);
} catch (InterruptedException _) {
}
return "商品ID:" + productId + ",名称:华为Mate70";
});
}
// 批量查询商品信息(100个商品)
public static List<String> batchGetProductInfo(List<String> productIds) {
List<CompletableFuture<String>> futures = new ArrayList<>();
for (String productId : productIds) {
futures.add(getProductInfo(productId));
}
// 等待所有任务完成,合并结果
CompletableFuture<Void> allFuture = CompletableFuture.allOf(
futures.toArray(new CompletableFuture[0])
);
// 合并结果
allFuture.join();
List<String> results = new ArrayList<>();
for (CompletableFuture<String> future : futures) {
results.add(future.join());
}
return results;
}
static void main() {
long start = System.currentTimeMillis();
// 模拟批量查询100个商品
List<String> productIds = new ArrayList<>();
for (int i = 0; i < 100; i++) {
productIds.add("P" + i);
}
List<String> results = batchGetProductInfo(productIds);
System.out.println("查询完成,共" + results.size() + "个商品");
System.out.println("总耗时:" + (System.currentTimeMillis() - start) + "ms");
}
}

王二纳闷:“单测好好的,怎么压测就崩了?”
哇哥把保温杯往桌上一墩:“你踩了 CompletableFuture 最经典的坑 —— 默认线程池!supplyAsync 默认用 ForkJoinPool.commonPool (),这个线程池的核心线程数是 CPU 核心数 - 1(比如 8 核 CPU 就是 7 个核心线程),100 个任务同时过来,线程池忙不过来就排队,高并发下队列堵满,线程数暴涨,CPU 直接干废!必须用自定义线程池,把线程数控制住!”
二、用 “工厂流水线” 讲透线程池优化逻辑

哇哥拿工厂流水线的例子打比方,王二瞬间懂了:

CompletableFuture 线程池优化 3 大核心(王二记小本本)
- 线程池隔离:不同业务用不同线程池(比如商品查询线程池、库存查询线程池),避免 “一个业务崩了影响所有业务”;
- 线程数合理配置:IO 密集型任务(比如查数据库、调用接口)线程数设为 CPU 核心数 * 2+1,CPU 密集型任务设为 CPU 核心数 + 1;
- 拒绝策略兜底:任务太多时,线程池满了要有拒绝策略(比如直接返回、排队、调用线程自己处理),避免线程池雪崩。
三、改造代码:自定义线程池 + 超时控制,稳如老狗
哇哥帮王二改了 3 处关键代码:加自定义线程池、加超时控制、加异常降级,改造后压测 1000 并发也稳如老狗。
➡️ 优化后的实战代码(可直接抄)
package cn.tcmeta.completables;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
/**
* @author: laoren
* @description: // 优化版:CompletableFuture+自定义线程池+超时控制+异常降级
* @version: 1.0.0
*/
public class CompletableFuturePracticalSampel {
// 1. 自定义线程池(IO密集型:CPU核心数*2+1)
private static final ThreadPoolExecutor PRODUCT_THREAD_POOL = new ThreadPoolExecutor(
10, // 核心线程数:10(8核CPU,8*2-6=10,按需调整)
20, // 最大线程数:20(核心线程不够时,最多再开10个临时线程)
60L, // 临时线程空闲时间:60秒(超时回收)
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100), // 任务队列:最多存100个任务
new ThreadFactory() {
private int count = 0;
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "tc-query-thread-" + (++count));
}
},
// 拒绝策略:任务太多时,让调用线程自己处理(避免任务丢失)
new ThreadPoolExecutor.CallerRunsPolicy()
);
// 2. 模拟查询单个商品信息(用自定义线程池,加超时+异常处理)
private static CompletableFuture<String> getProductInfo(String productId) {
// 核心:指定自定义线程池
return CompletableFuture.supplyAsync(() -> {
try {
// 模拟服务调用耗时
TimeUnit.MILLISECONDS.sleep(50);
// 模拟10%的概率抛异常
if (Math.random() < 0.1) {
throw new RuntimeException("服务超时");
}
return "商品ID:" + productId + ",名称:华为Mate70,价格:6999元";
} catch (InterruptedException e) {
throw new RuntimeException("线程被中断");
}
}, PRODUCT_THREAD_POOL)
// 3. 超时控制:300ms没返回就超时(避免任务一直阻塞)
.orTimeout(300, TimeUnit.MILLISECONDS)
// 4. 异常降级:超时或服务异常时返回默认值
.exceptionally(ex -> {
System.out.println("商品" + productId + "查询异常:" + ex.getMessage());
return "商品ID:" + productId + ",查询失败(已自动补货)";
});
}
// 批量查询商品信息(优化版)
public static List<String> batchGetProductInfo(List<String> productIds) {
List<CompletableFuture<String>> futures = new ArrayList<>();
for (String productId : productIds) {
futures.add(getProductInfo(productId));
}
// 等待所有任务完成(可加总超时)
CompletableFuture<Void> allFuture = CompletableFuture.allOf(
futures.toArray(new CompletableFuture[0])
);
// 总超时控制:500ms内没完成就中断
try {
allFuture.get(500, TimeUnit.MILLISECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
System.out.println("批量查询超时,中断所有任务");
// 中断所有任务(可选,根据业务场景)
futures.forEach(future -> future.cancel(true));
}
// 合并结果(过滤掉null)
List<String> results = new ArrayList<>();
for (CompletableFuture<String> future : futures) {
if (!future.isCancelled() && future.isDone()) {
results.add(future.join());
}
}
return results;
}
// 关闭线程池(JVM退出时调用)
public static void shutdownThreadPool() {
PRODUCT_THREAD_POOL.shutdown();
try {
if (!PRODUCT_THREAD_POOL.awaitTermination(1, TimeUnit.MINUTES)) {
PRODUCT_THREAD_POOL.shutdownNow();
}
} catch (InterruptedException e) {
PRODUCT_THREAD_POOL.shutdownNow();
}
}
static void main() {
try {
long start = System.currentTimeMillis();
// 模拟批量查询100个商品
List<String> productIds = new ArrayList<>();
for (int i = 0; i < 100; i++) {
productIds.add("P" + i);
}
List<String> results = batchGetProductInfo(productIds);
System.out.println("查询完成,共" + results.size() + "个商品");
System.out.println("总耗时:" + (System.currentTimeMillis() - start) + "ms");
} finally {
// 程序结束时关闭线程池
shutdownThreadPool();
}
}
}

核心优化点(哇哥划重点)
- 自定义线程池:命名线程(便于日志排查)、控制核心 / 最大线程数、设置队列容量和拒绝策略,避免资源耗尽;
- 超时控制:用orTimeout给单个任务设超时(300ms),用get(timeout)给批量任务设总超时(500ms),避免任务无限阻塞;
- 异常降级:exceptionally处理服务异常和超时,返回友好提示,不影响整体结果;
线程池关闭:JVM 退出时关闭线程池,避免线程泄露。
四、实战进阶:批量异步 + 结果聚合
哇哥带着王二写了更贴近生产的代码 —— 批量查询商品信息,同时过滤无效数据、按价格排序,直接能用到项目里。

📢 在生产级批量处理代码(可直接抄)
package cn.tcmeta.completables;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* @author: laoren
* @description: TODO
* @version: 1.0.0
*/
public class CompletableFutureBatchAdvancedSample {
// 商品实体类
static class Product {
private String id;
private String name;
private double price;
private boolean valid; // 是否有效
// 构造器、getter、setter
public Product(String id, String name, double price, boolean valid) {
this.id = id;
this.name = name;
this.price = price;
this.valid = valid;
}
public String getId() {
return id;
}
public String getName() {
return name;
}
public double getPrice() {
return price;
}
public boolean isValid() {
return valid;
}
}
// 自定义线程池(商品查询专用)
private static final ThreadPoolExecutor PRODUCT_POOL = new ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors() * 2,
Runtime.getRuntime().availableProcessors() * 4,
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(200),
r -> new Thread(r, "product-batch-thread-"),
new ThreadPoolExecutor.CallerRunsPolicy()
);
// 查询单个商品(返回Product实体,含有效性标记)
private static CompletableFuture<Product> queryProduct(String productId) {
return CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.MILLISECONDS.sleep((int) (Math.random() * 50));
// 模拟无效商品(ID为偶数)
if (Integer.parseInt(productId.substring(1)) % 2 == 0) {
return new Product(productId, "无效商品", 0, false);
}
// 模拟正常商品
double price = 5000 + Math.random() * 2000; // 5000-7000元
return new Product(productId, "华为Mate70", price, true);
} catch (InterruptedException e) {
return new Product(productId, "查询异常", 0, false);
}
}, PRODUCT_POOL)
.orTimeout(100, TimeUnit.MILLISECONDS)
.exceptionally(ex -> new Product(productId, "超时异常", 0, false));
}
// 批量查询 + 过滤 + 排序
public static List<Product> batchQueryAndProcess(List<String> productIds) {
// 1. 批量发起异步任务
List<CompletableFuture<Product>> futures = productIds.stream()
.map(CompletableFutureBatchAdvancedSample::queryProduct)
.toList();
// 2. 等待所有任务完成
CompletableFuture<Void> allFuture = CompletableFuture.allOf(
futures.toArray(new CompletableFuture[0])
);
// 3. 聚合结果:过滤无效商品,按价格降序排序
return allFuture.thenApply(v -> futures.stream()
.map(CompletableFuture::join)
.filter(Product::isValid) // 过滤无效商品
.sorted((p1, p2) -> Double.compare(p2.getPrice(), p1.getPrice())) // 按价格降序
.collect(Collectors.toList()))
.join();
}
static void main() {
try {
// 模拟10个商品ID
List<String> productIds = Arrays.asList("P0", "P1", "P2", "P3", "P4", "P5", "P6", "P7", "P8", "P9");
List<Product> result = batchQueryAndProcess(productIds);
// 打印结果
System.out.println("批量查询结果(有效商品,按价格降序):");
for (Product product : result) {
System.out.printf("商品ID:%s,名称:%s,价格:%.2f元%n",
product.getId(), product.getName(), product.getPrice());
}
} finally {
PRODUCT_POOL.shutdown();
}
}
}

五、面试必问:CompletableFuture 实战优化题(附答案)

哇哥整理了 3 道实战优化面试题,王二背完直接拿捏面试官!
➡️ 面试题 1:CompletableFuture 为什么要自定义线程池?默认线程池有什么问题?
答案:
-
必须自定义线程池,默认线程池(ForkJoinPool.commonPool ())有 3 个致命问题:
资源争抢:默认线程池是共享的,所有业务都用它,一个业务任务暴增会占用所有线程,影响其他业务; -
线程数固定:核心线程数 = CPU 核心数 - 1,IO 密集型任务(比如调用接口)会因为等待 IO 而空闲,导致任务排队;
-
排查困难:默认线程池的线程没有自定义命名,日志中无法区分是哪个业务的线程,排查问题极难。
优化方案:
一每个业务用独立的自定义线程池,命名线程、控制核心 / 最大线程数、设置队列和拒绝策略。
✔️ 面试题 2:CompletableFuture 如何设置超时时间?单个任务和批量任务分别怎么处理?
答案:
- 单个任务超时:用orTimeout(long timeout, TimeUnit unit)或completeOnTimeout(T value, long timeout, TimeUnit unit);
- orTimeout:超时抛出 TimeoutException;
- completeOnTimeout:超时返回默认值,不抛异常。
- 批量任务超时:用CompletableFuture.allOf().get(long timeout, TimeUnit unit),超时后可调用cancel(true)中断所有任务。
// 单个任务超时:300ms超时返回默认值
CompletableFuture<String> singleFuture = CompletableFuture.supplyAsync(() -> "结果")
.completeOnTimeout("超时默认值", 300, TimeUnit.MILLISECONDS);
// 批量任务超时:500ms总超时
CompletableFuture<Void> allFuture = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
try {
allFuture.get(500, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
futures.forEach(f -> f.cancel(true)); // 中断所有任务
}
✅ 面试题 3:CompletableFuture 的线程池怎么关闭?不关闭有什么问题?
答案:
关闭方式:
- shutdown():平缓关闭,等待已提交的任务完成,不再接收新任务;
- shutdownNow():立即关闭,中断正在执行的任务,返回未执行的任务;
- 实战中推荐 “ shutdown ()+awaitTermination ()” 组合,确保任务完成后关闭。
- 不关闭的问题:线程池的核心线程是常驻的,不关闭会导致线程泄露,JVM 无法正常退出。
// 正确关闭线程池
public static void shutdown(ThreadPoolExecutor pool) {
pool.shutdown();
try {
// 等待1分钟,让任务完成
if (!pool.awaitTermination(1, TimeUnit.MINUTES)) {
// 1分钟后还没完成,强制关闭
List<Runnable> unfinishedTasks = pool.shutdownNow();
System.out.println("未完成的任务数:" + unfinishedTasks.size());
}
} catch (InterruptedException e) {
pool.shutdownNow();
} catch (InterruptedException e) {
pool.shutdownNow (); // 捕获中断异常,强制关闭
Thread.currentThread ().interrupt (); // 保留中断状态,供上层处理
}
}
六、总结:CompletableFuture实战封神心法(王二编顺口溜)
王二把三天学到的核心知识点编成顺口溜,贴满了显示器边框,生怕再忘:
- 异步别用Future等,CompletableFuture真能赢;
- 线程池要自定义,隔离配置记心里;
- IO密集线程多,CPU密集别超核;
- 超时降级不能少,异常日志要记好;
- allOf批量等所有,anyOf取快不发愁;
- 线程池用完关,资源不泄露才心安。
👉 哇哥的实战彩蛋
“我当年做电商秒杀,用CompletableFuture批量处理订单,”哇哥突然压低声音,“一开始没加超时控制,有个第三方物流接口卡了30秒,导致1000个订单全堵在异步任务里,用户付了钱却看不到订单状态,客服电话被打爆——后来加了orTimeout(500ms),超时直接返回‘物流查询中’,虽然体验差一点,但至少不会堵死整个系统。”
他拍了拍王二的肩膀:“CompletableFuture不是银弹,实战里既要用它的异步优势,也要防着它的坑——自定义线程池、超时控制、异常降级,这三样是生产环境的‘保命符’,少一个都可能出大问题。”
王二点头如捣蒜,把哇哥的话记在小本本上——这三天的学习,他从“用Future阻塞卡爆接口”,到“用CompletableFuture链式调用提速”,再到“自定义线程池扛住高并发”,终于把异步编程玩明白了。
关注我,下次咱们扒一扒CompletableFuture和Spring的结合用法——怎么用@Async注解简化异步代码?怎么和Spring事务搭配?怎么用Spring监控异步任务状态?让你在Spring项目里把异步编程用得炉火纯青,成为团队里的“异步大神”!


1159

被折叠的 条评论
为什么被折叠?



