【041】CompletableFuture 实战封神!批量异步 + 线程池优化直接抄

📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌

📙 作者: 编程技术圈(哇哥面试陪跑)
👉 欢迎关注、分享、评论
✔️ 持续分享更多干货内容
🌐🌏🌎➕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实战封神心法(王二编顺口溜)

王二把三天学到的核心知识点编成顺口溜,贴满了显示器边框,生怕再忘:

  1. 异步别用Future等,CompletableFuture真能赢
  2. 线程池要自定义,隔离配置记心里
  3. IO密集线程多,CPU密集别超核
  4. 超时降级不能少,异常日志要记好
  5. allOf批量等所有,anyOf取快不发愁
  6. 线程池用完关,资源不泄露才心安

👉 哇哥的实战彩蛋

“我当年做电商秒杀,用CompletableFuture批量处理订单,”哇哥突然压低声音,“一开始没加超时控制,有个第三方物流接口卡了30秒,导致1000个订单全堵在异步任务里,用户付了钱却看不到订单状态,客服电话被打爆——后来加了orTimeout(500ms),超时直接返回‘物流查询中’,虽然体验差一点,但至少不会堵死整个系统。”

他拍了拍王二的肩膀:“CompletableFuture不是银弹,实战里既要用它的异步优势,也要防着它的坑——自定义线程池、超时控制、异常降级,这三样是生产环境的‘保命符’,少一个都可能出大问题。”

王二点头如捣蒜,把哇哥的话记在小本本上——这三天的学习,他从“用Future阻塞卡爆接口”,到“用CompletableFuture链式调用提速”,再到“自定义线程池扛住高并发”,终于把异步编程玩明白了。

关注我,下次咱们扒一扒CompletableFuture和Spring的结合用法——怎么用@Async注解简化异步代码?怎么和Spring事务搭配?怎么用Spring监控异步任务状态?让你在Spring项目里把异步编程用得炉火纯青,成为团队里的“异步大神”!

在这里插入图片描述

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值