【044】Executors 是陷阱!Executor 实战优化,生产环境不翻车的秘诀

在这里插入图片描述


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

📙 作者: 编程技术圈(哇哥面试陪跑)
👉 欢迎关注、分享、评论
✔️ 持续分享更多干货内容
🌐🌏🌎➕tcmeta, 欢迎沟通交流

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


零、引入

王二的订单系统又崩了。这次不是线程太多,是用了 Executors.newCachedThreadPool (),高峰期日志里全是 “OutOfMemoryError: unable to create new native thread”—— 线程数飙到了几万,JVM 内存直接炸了。领导把他骂得狗血淋头:“上次让你学 ThreadPoolExecutor,你怎么还在用 Executors?再出问题,你就卷铺盖走人!”

王二抱着头蹲在工位旁,哇哥走过来,踢了踢他的凳子:“早告诉你 Executors 是陷阱,你偏不听。今天教你 Executor 的实战优化,从批量任务处理到线程池监控,全是生产环境能用的干货,再翻车我替你背锅。”

点赞 + 关注,跟着哇哥和王二,吃透 Executor 实战技巧,生产环境再也不踩坑!

在这里插入图片描述

一、王二的致命坑:Executors 的 “甜蜜陷阱”

在这里插入图片描述
王二的订单系统,用 Executors.newCachedThreadPool () 处理批量订单,代码长这样:

package cn.tcmeta.threadpoolexecutor;


import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * @author: laoren
 * @description: 王二的坑:用Executors.newCachedThreadPool导致OOM
 * @version: 1.0.0
 */
public class ExecutorsDisasterSample {

    // 处理订单任务
    private static void processOrder(String orderId) {
        try {
            TimeUnit.MILLISECONDS.sleep(100); // 模拟处理耗时
            System.out.println(Thread.currentThread().getName() + ":处理订单" + orderId);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    static void main() {
        // 陷阱:newCachedThreadPool的最大线程数是Integer.MAX_VALUE
        ExecutorService executor = Executors.newCachedThreadPool();

        // 模拟10000个订单任务(高峰期会更多)
        for (int i = 0; i < 10000; i++) {
            String orderId = "ORDER_" + i;
            executor.submit(() -> processOrder(orderId));
        }

        executor.shutdown();
    }

}

运行起来,线程数从几十涨到几万,很快就 OOM 了。“newCachedThreadPool 不是‘缓存线程池’吗?怎么会创建这么多线程?” 王二不解。

“缓存线程池是‘缓存空闲线程’,不是‘限制线程数’,” 哇哥恨铁不成钢,“它的 corePoolSize 是 0,maximumPoolSize 是 Integer.MAX_VALUE,任务一来就创建新线程,空闲 60 秒才销毁。高峰期 10000 个任务同时来,就创建 10000 个线程,内存不炸才怪 —— 这就是 Executors 的陷阱,看似方便,实则埋了雷。”

二、用 “医院挂号” 讲透 Executor 实战优化:可控才是王道

哇哥拿医院挂号的例子,给王二讲实战优化的核心:“医院不会无限制加挂号窗口(线程),而是会设置号源池(队列)、挂号员数量(核心线程),人太多就告诉患者‘号满了’(拒绝策略)——Executor 优化也是一个道理:控制线程数、限制队列容量、做好监控。”

在这里插入图片描述

💯 Executor 实战优化 4 大核心(王二记在病历本上)

  • 拒绝 Executors,用自定义 ThreadPoolExecutor:手动控制核心参数,避免 OOM;
  • 线程池隔离:不同业务用不同线程池(比如订单线程池、日志线程池),避免一个业务崩了影响全局;
  • 批量任务处理:用 invokeAll、invokeAny 处理批量任务,提高效率;
  • 线程池监控:监控线程池状态(活跃线程数、队列任务数),提前发现问题。

三、实战 1:线程池隔离 + 批量任务处理(可直接抄)

在这里插入图片描述
哇哥帮王二改造了订单系统,用线程池隔离和 invokeAll 处理批量订单,代码如下:

package cn.tcmeta.threadpoolexecutor;


import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

/**
 * @author: laoren
 * @description: 实战:线程池隔离+批量任务处理
 * @version: 1.0.0
 */
public class ExecutorBatchSample {

    // 1. 订单业务专用线程池(隔离)
    private static final ThreadPoolExecutor ORDER_THREAD_POOL = new ThreadPoolExecutor(
            10,          // 核心线程数(CPU核心数*2)
            20,          // 最大线程数
            60,          // 空闲时间60秒
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(200), // 有界队列,容量200
            r -> new Thread(r, "order-pool-"),
            new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:调用线程自己处理
    );

    // 2. 日志业务专用线程池(隔离,和订单池互不干扰)
    private static final ThreadPoolExecutor LOG_THREAD_POOL = new ThreadPoolExecutor(
            5,
            10,
            60,
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(100),
            r -> new Thread(r, "log-pool-"),
            new ThreadPoolExecutor.DiscardOldestPolicy()
    );


    // 处理单个订单(有返回值,用Callable)
    private static Callable<String> processOrderTask(String orderId) {
        return () -> {
            TimeUnit.MILLISECONDS.sleep(100);
            String result = Thread.currentThread().getName() + ":订单" + orderId + "处理完成";
            // 处理完订单,记录日志(用日志线程池)
            LOG_THREAD_POOL.submit(() -> logOrderResult(orderId, result));
            return result;
        };
    }

    // 记录订单处理日志
    private static void logOrderResult(String orderId, String result) {
        try {
            TimeUnit.MILLISECONDS.sleep(50);
            System.out.println(Thread.currentThread().getName() + ":日志 - " + result);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    // 批量处理订单
    public static List<String> batchProcessOrders(List<String> orderIds) throws ExecutionException, InterruptedException {
        // 1. 封装所有任务为Callable列表
        List<Callable<String>> tasks = new ArrayList<>();
        for (String orderId : orderIds) {
            tasks.add(processOrderTask(orderId));
        }

        // 2. 批量提交任务:invokeAll等待所有任务完成,返回Future列表
        List<Future<String>> futures = ORDER_THREAD_POOL.invokeAll(tasks);

        // 3. 解析结果
        List<String> results = new ArrayList<>();
        for (Future<String> future : futures) {
            if (!future.isCancelled()) {
                results.add(future.get());
            }
        }
        return results;
    }

    // 关闭所有线程池(JVM退出时调用)
    public static void shutdownAllPools() {
        shutdownPool(ORDER_THREAD_POOL, "订单线程池");
        shutdownPool(LOG_THREAD_POOL, "日志线程池");
    }

    private static void shutdownPool(ThreadPoolExecutor pool, String name) {
        pool.shutdown();
        try {
            if (!pool.awaitTermination(1, TimeUnit.MINUTES)) {
                List<Runnable> unfinished = pool.shutdownNow();
                System.out.println(name + "未完成任务数:" + unfinished.size());
            }
            System.out.println(name + "已关闭");
        } catch (InterruptedException e) {
            pool.shutdownNow();
            Thread.currentThread().interrupt();
        }
    }

    static void main() {
        try {
            long start = System.currentTimeMillis();
            // 模拟批量处理100个订单
            List<String> orderIds = new ArrayList<>();
            for (int i = 0; i < 100; i++) {
                orderIds.add("ORDER_" + i);
            }

            List<String> results = batchProcessOrders(orderIds);
            System.out.println("===== 批量处理结果 =====");
            results.forEach(System.out::println);
            System.out.println("总耗时:" + (System.currentTimeMillis() - start) + "ms");
        } catch (ExecutionException | InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            shutdownAllPools();
        }
    }

}

在这里插入图片描述
王二看着结果,算了算:“100 个订单,每个耗时 100ms,用 10 个核心线程,总耗时 1050ms,刚好是 10 个批次,效率太高了!”

“这就是 invokeAll 的好处,” 哇哥说,“批量提交任务,线程池自动分配线程处理,比你一个个 submit 高效多了。而且订单和日志用不同线程池,就算日志线程池满了,也不会影响订单处理。”

四、实战 2:线程池监控,提前发现问题

在这里插入图片描述
“生产环境里,线程池不能‘黑盒运行’,” 哇哥说,“必须监控它的状态,比如活跃线程数、队列任务数,一旦超过阈值就报警 —— 就像医院监控挂号人数,快满了就加临时窗口。”

✨ 线程池监控工具类(可直接集成到项目)

在这里插入图片描述

package cn.tcmeta.threadpoolexecutor;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @author: laoren
 * @description: 线程池监控工具类
 * @version: 1.0.0
 */
public class ThreadPoolMonitor {

    private static final double ACTIVE_THREAD_THRESHOLD_RATIO = 0.8;
    private static final int QUEUE_REMAINING_CAPACITY_ALARM_LIMIT = 10;
    private static final long MONITOR_INTERVAL_SECONDS = 10L;

    // 监控线程池状态,每隔10秒打印一次
    public static void monitorThreadPool(ThreadPoolExecutor pool, String poolName) {
        if (pool == null || poolName == null || poolName.isEmpty()) {
            throw new IllegalArgumentException("线程池对象及名称不能为空");
        }

        Runnable monitorTask = () -> {
            while (!pool.isShutdown() && !Thread.currentThread().isInterrupted()) {
                try {
                    // 获取线程池状态信息
                    int corePoolSize = pool.getCorePoolSize();
                    int activeCount = pool.getActiveCount();
                    int maximumPoolSize = pool.getMaximumPoolSize();
                    long taskCount = pool.getTaskCount();
                    long completedTaskCount = pool.getCompletedTaskCount();
                    int queueSize = pool.getQueue() != null ? pool.getQueue().size() : 0;

                    int queueRemainingCapacity = -1;
                    if (pool.getQueue() instanceof ArrayBlockingQueue) {
                        queueRemainingCapacity = ((ArrayBlockingQueue<?>) pool.getQueue()).remainingCapacity();
                    }

                    // 打印监控信息
                    System.out.printf("[%s监控] 核心线程数:%d,活跃线程数:%d,最大线程数:%d," +
                                    "总任务数:%d,已完成任务数:%d,队列任务数:%d,队列剩余容量:%d%n",
                            poolName, corePoolSize, activeCount, maximumPoolSize,
                            taskCount, completedTaskCount, queueSize, queueRemainingCapacity);

                    // 模拟报警逻辑:活跃线程数超过最大线程数的80%,或者队列剩余容量小于10
                    if (activeCount > maximumPoolSize * ACTIVE_THREAD_THRESHOLD_RATIO ||
                            (queueRemainingCapacity != -1 && queueRemainingCapacity < QUEUE_REMAINING_CAPACITY_ALARM_LIMIT)) {
                        System.out.println("【报警】" + poolName + "压力过大,请及时处理!");
                    }

                    TimeUnit.SECONDS.sleep(MONITOR_INTERVAL_SECONDS); // 每隔10秒监控一次
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                } catch (Exception ex) {
                    // 忽略非中断异常,保证监控持续运行
                }
            }
        };

        Thread monitorThread = new Thread(monitorTask, "thread-pool-monitor-" + poolName);
        monitorThread.setDaemon(true); // 设置为守护线程,随主线程退出而退出
        monitorThread.start();
    }

    // 测试监控
    static void main() throws InterruptedException {
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                5, 10, 60, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(20),
                r -> new Thread(r, "test-pool-")
        );

        try {
            // 启动监控
            monitorThreadPool(pool, "测试线程池");

            // 提交100个任务,模拟压力
            for (int i = 0; i < 100; i++) {
                int taskId = i;
                pool.submit(() -> {
                    try {
                        TimeUnit.MILLISECONDS.sleep(100);
                        System.out.println(Thread.currentThread().getName() + ":处理任务" + taskId);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                });

                // 控制提交速率,避免瞬间提交过多任务
                if (i % 5 == 0) {
                    TimeUnit.MILLISECONDS.sleep(50);
                }
            }

            // 等待任务处理
            TimeUnit.SECONDS.sleep(30);
        } finally {
            pool.shutdown(); // 确保无论如何都会尝试关闭线程池
        }
    }
}

  • 执行结果
[测试线程池监控] 核心线程数:5,活跃线程数:0,最大线程数:10,总任务数:0,已完成任务数:0,队列任务数:0,队列剩余容量:20
...
[测试线程池监控] 核心线程数:5,活跃线程数:0,最大线程数:10,总任务数:100,已完成任务数:100,队列任务数:0,队列剩余容量:20
[测试线程池监控] 核心线程数:5,活跃线程数:0,最大线程数:10,总任务数:100,已完成任务数:100,队列任务数:0,队列剩余容量:20
[测试线程池监控] 核心线程数:5,活跃线程数:0,最大线程数:10,总任务数:100,已完成任务数:100,队列任务数:0,队列剩余容量:20

“有了这个监控,线程池压力大的时候,我们能提前发现,不用等系统崩了才救火,” 王二兴奋地说,“这就像给线程池装了个‘体温计’,一发烧就报警。”

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

在这里插入图片描述
哇哥整理了 3 道实战优化面试题,王二背完直呼 “稳了”:

🔔 面试题 1:如何实现线程池隔离?为什么要隔离?

答案:

  • 实现方式:不同业务创建独立的 ThreadPoolExecutor,比如订单线程池、支付线程池、日志线程池,线程名、核心参数、队列、拒绝策略都单独配置。
  • 隔离原因:避免 “一个业务故障拖垮整个系统”,比如日志线程池满了,不会影响订单处理;支付线程池出问题,不会影响商品查询。

✔️ 面试题 2:Executor 如何处理批量任务?invokeAll 和 invokeAny 有什么区别?

答案:

  • 批量处理方式:用 ExecutorService 的 invokeAll 和 invokeAny 方法,封装多个 Callable 任务为列表提交。

区别:

  • invokeAll:等待所有任务完成,返回 Future 列表,适合 “所有任务都要处理” 的场景(比如批量订单处理);
  • invokeAny:等待任意一个任务完成,返回该任务的结果,适合 “取最快结果” 的场景(比如多渠道查询商品价格)。

😭 面试题 3:如何监控 ThreadPoolExecutor 的状态?核心监控指标有哪些?

监控方式:通过 ThreadPoolExecutor 的 API 获取状态信息,定时打印或集成到监控系统(比如 Prometheus+Grafana)。

核心监控指标:

  • 活跃线程数(getActiveCount):反映线程池当前压力;
  • 队列任务数(getQueue ().size ()):反映任务堆积情况;
  • 总任务数 / 已完成任务数(getTaskCount/getCompletedTaskCount):反映线程池吞吐量;
  • 核心线程数 / 最大线程数(getCorePoolSize/getMaximumPoolSize):确认配置是否合理。

总结:Executor 实战封神心法(王二刻在键盘上)

在这里插入图片描述

  • Executors 是陷阱,自定义线程池才放心:核心参数自己配,有界队列防 OOM;
  • 线程池要隔离,业务之间不扯皮:订单、支付、日志各用各的,故障影响小;
  • 批量任务用 invoke,效率提升不用吹:invokeAll 等所有,invokeAny 取最快;
  • 监控不能少,报警要趁早:活跃线程、队列任务数,异常及时处理;
  • 用完线程池, shutdown 要记牢:平缓关闭加等待,避免任务丢了跑。

📌 哇哥的终极大招

“最后送你一句口诀,” 哇哥拍了拍王二的肩膀,“‘核心参数配得好,线程隔离少不了,批量处理用 invoke,监控报警要趁早’—— 把这句话记牢,生产环境用 Executor,保你不翻车。”

王二点了点头,把优化后的代码部署到测试环境,压测 1000 并发,线程池状态稳定,响应时间控制在 200ms 以内 —— 他终于不再是那个只会 new Thread 的菜鸟了。

关注我,下次咱们扒一扒 Executor 和 Spring 的结合用法 —— 怎么用 @Async 注解简化异步代码?怎么和 Spring 事务搭配?怎么用 Spring Boot 监控线程池?让你在 Spring 项目里把 Executor 用得炉火纯青,成为团队里的并发高手!

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值