【043】面试官追着问 ThreadPoolExecutor?用车间流水线讲透,再也不慌!

在这里插入图片描述


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

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

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


零、引入

王二从面试间出来,脸比锅底还黑。面试官最后抛了个问题:“ThreadPoolExecutor 的核心参数有哪些?工作原理是什么?” 他支支吾吾,只说出个 “核心线程数”,后面的全卡壳了 —— 心仪的岗位就这么飞了。

回到工位,哇哥正对着一份线程池监控报表皱眉,见王二这模样,便知是怎么回事。“ThreadPoolExecutor 是 Executor 框架的骨头,面试必考,你连骨头都没啃动,怎么能过?” 哇哥把报表推到他面前,“今天用车间流水线的道理,把这东西讲透,下次再被问,你就把面试官说懵。”

点赞 + 关注,跟着哇哥和王二,吃透 ThreadPoolExecutor 的核心,并发面试的半壁江山就稳了!

在这里插入图片描述

一、王二的新坑:只知用 Executors,不知 ThreadPoolExecutor

在这里插入图片描述

王二之前用 Executor,全靠 Executors 工具类,比如Executors.newFixedThreadPool(10),从来没深究过背后的 ThreadPoolExecutor。面试官一追问 “FixedThreadPool 的核心参数怎么设置的”,他就露怯了。

“Executors 是给新手用的拐杖,” 哇哥嗤笑一声,“它帮你封装了 ThreadPoolExecutor 的参数,但生产环境里,这拐杖迟早把你绊倒。你得知道 ThreadPoolExecutor 的构造方法,那才是真东西。”

➡️ ThreadPoolExecutor 的 “命脉”:7 个核心参数

在这里插入图片描述

哇哥打开 JDK 源码,指着 ThreadPoolExecutor 的构造方法:

// ThreadPoolExecutor的核心构造方法
public ThreadPoolExecutor(
    int corePoolSize,        // 核心线程数
    int maximumPoolSize,     // 最大线程数
    long keepAliveTime,      // 非核心线程空闲时间
    TimeUnit unit,           // 空闲时间单位
    BlockingQueue<Runnable> workQueue, // 任务队列
    ThreadFactory threadFactory,       // 线程工厂
    RejectedExecutionHandler handler   // 拒绝策略
) {
    // ... 初始化逻辑
}

“这 7 个参数,就是车间的‘管理制度’,” 哇哥拿车间流水线类比,一下就把抽象参数讲活了:

在这里插入图片描述
王二恍然大悟:“原来 newFixedThreadPool (10),就是把 corePoolSize 和 maximumPoolSize 都设为 10,相当于车间全是正式工,没有临时工!”

“总算开窍了,” 哇哥点头,“Executors.newFixedThreadPool (n) 的底层,就是 new ThreadPoolExecutor (n, n, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>())—— 它帮你填了参数,但你得知道填的是什么。”

二、ThreadPoolExecutor 工作原理:流水线怎么处理零件?

在这里插入图片描述

“知道了参数,还得懂工作流程,” 哇哥拿一支笔当零件,在桌上演示,“当一个任务(零件)过来,流水线(线程池)是这么处理的:

  • 先看正式工(核心线程) 有没有空:有空就安排正式工干;
  • 正式工都忙了,就把零件放进料箱(任务队列);
  • 料箱也满了,就招临时工(非核心线程)来干;
  • 正式工 + 临时工都满了,料箱也满了,就执行拒绝策略(比如告诉送零件的‘别送了,放不下了’)。”

👉 工作流程

在这里插入图片描述

✔️ 拒绝策略:料箱满了怎么办?(面试高频)

哇哥强调:“拒绝策略是面试必问,就像料箱满了,车间总得有个说法。JDK 默认提供 4 种拒绝策略:”

在这里插入图片描述

  • AbortPolicy(默认):直接抛RejectedExecutionException异常,简单粗暴;
  • CallerRunsPolicy:让提交任务的线程自己执行(比如送零件的人自己动手干),缓解压力;
  • DiscardPolicy:悄悄丢弃任务,不抛异常(风险高,慎用);
  • DiscardOldestPolicy:丢弃队列里最老的任务,再把新任务加进去。

“生产环境里,别用默认的 AbortPolicy,” 哇哥提醒,“比如秒杀场景,任务满了直接抛异常,用户体验太差,用 CallerRunsPolicy 或者自定义拒绝策略(比如记录日志,返回‘系统繁忙,请重试’)更友好。”

三、实战:自定义 ThreadPoolExecutor,掌控一切

在这里插入图片描述
王二照着哇哥的指导,写了个自定义线程池的代码,把 7 个参数都配置了一遍,还加了自定义拒绝策略:

package cn.tcmeta.threadpoolexecutor;


import java.util.concurrent.*;

/**
 * @author: laoren
 * @description: 自定义ThreadPoolExecutor,生产环境可用
 * @version: 1.0.0
 */
public class CustomThreadSample {
    static void main() {
        // 1. 核心参数配置
        int corePoolSize = 5;         // 正式工5人
        int maximumPoolSize = 10;     // 最多10人(5正式+5临时)
        long keepAliveTime = 60;      // 临时工空闲60秒解雇
        TimeUnit unit = TimeUnit.SECONDS; // 时间单位:秒

        // 2. 任务队列:容量100的阻塞队列
        BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(100);

        // 3. 线程工厂:给线程命名,便于排查问题
        ThreadFactory threadFactory = new ThreadFactory() {
            private int count = 0;

            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r);
                thread.setName("order-thread-" + (++count)); // 线程名:订单处理线程-1
                return thread;
            }
        };

        // 4. 自定义拒绝策略:记录日志+返回友好提示
        RejectedExecutionHandler rejectedHandler = (r, executor) -> {
            // 记录任务拒绝日志
            System.out.println("任务" + r.toString() + "被拒绝,当前线程池状态:" +
                    "核心线程数=" + executor.getCorePoolSize() +
                    ",活跃线程数=" + executor.getActiveCount() +
                    ",队列任务数=" + executor.getQueue().size());
            // 实际项目中可以抛自定义异常,让上层返回“系统繁忙”
            throw new RejectedExecutionException("系统繁忙,请稍后重试");
        };

        // 5. 创建自定义线程池
        try (ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
                corePoolSize,
                maximumPoolSize,
                keepAliveTime,
                unit,
                workQueue,
                threadFactory,
                rejectedHandler
        );) {
            // 6. 提交任务(模拟120个订单任务)
            for (int i = 0; i < 120; i++) {
                int orderId = i;
                threadPool.submit(() -> {
                    try {
                        TimeUnit.MILLISECONDS.sleep(100); // 模拟处理订单耗时
                        System.out.println(Thread.currentThread().getName() + ":处理订单" + orderId + "完成");
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                });
            }

            // 7. 关闭线程池
            threadPool.shutdown();
        }
    }
}

在这里插入图片描述
王二看着清晰的线程名和拒绝日志,感慨道:“这下出了问题,我能直接定位到是‘订单处理线程’的问题,比之前的 Thread-1 好多了!”

“这就是自定义线程池的好处,” 哇哥说,“线程命名、拒绝策略、队列容量,全在你掌控之中,生产环境出问题也能快速排查。”

四、面试高频题:ThreadPoolExecutor 核心问题(附答案)

在这里插入图片描述
哇哥整理了 3 道面试必考题,王二抄在小本本上,背得滚瓜烂熟:

➡️ 面试题 1:ThreadPoolExecutor 的 corePoolSize 和 maximumPoolSize 有什么区别?怎么设置?

答案:

  • 区别:corePoolSize 是核心线程数(正式工),即使空闲也不销毁;maximumPoolSize 是核心线程 + 非核心线程的总数(正式工 + 临时工),非核心线程空闲到 keepAliveTime 会销毁。

  • 设置依据:

    • CPU 密集型任务(比如计算):核心线程数 = CPU 核心数 + 1,避免线程切换开销;
    • IO 密集型任务(比如调用接口、查数据库):核心线程数 = CPU 核心数 * 2+1,因为线程大部分时间在等 IO,多开线程能提高利用率。

📢 面试题 2:ThreadPoolExecutor 的任务队列有哪些选择?分别用在什么场景?

答案:
常用的阻塞队列有 3 种:

  • LinkedBlockingQueue(链表队列):无界队列(默认),适合任务量稳定的场景,但任务过多会导致 OOM;
  • ArrayBlockingQueue(数组队列):有界队列,适合控制任务数量的场景,配合拒绝策略使用,避免 OOM;
  • SynchronousQueue(同步队列):零容量队列,任务必须马上被线程处理,适合实时性要求高的场景(比如秒杀),对应 Executors.newCachedThreadPool ()。

✅ 面试题 3:为什么不推荐用 Executors 创建线程池?

Executors 封装的线程池有 3 个致命问题,生产环境慎用

  • newFixedThreadPool/newSingleThreadExecutor:用 LinkedBlockingQueue(无界队列),任务过多会导致 OOM;
  • newCachedThreadPool:maximumPoolSize 是 Integer.MAX_VALUE,任务过多会创建大量线程,导致 OOM;
  • newScheduledThreadPool:核心线程数固定,任务队列无界,同样有 OOM 风险。

解决方案:
用 ThreadPoolExecutor 自定义线程池,手动设置核心参数、有界队列和拒绝策略。

五、总结:ThreadPoolExecutor 核心心法(王二编的顺口溜)

在这里插入图片描述

七参数是命脉,流水线来类比;
核心线程是正式工,最大线程含临时;
队列是料箱,满了招临时;
拒绝策略要选好,用户体验差不了;
Executors 是拐杖,自定义才是真章。

❓ 哇哥的血泪教训

“我刚工作时,用 Executors.newCachedThreadPool 处理日志,” 哇哥回忆道,“有次系统出 bug,日志量暴增,线程池创建了几千个线程,JVM 直接 OOM 挂了。后来换成自定义线程池,队列设为 1000,拒绝策略用 CallerRunsPolicy,就算日志再多,也不会把系统拖垮 —— 这都是血的教训。”

关注我,下一篇咱们扒一扒 Executor 框架的实战优化 —— 怎么用 Executor 处理批量任务?线程池怎么监控?生产环境里,怎么防止线程池 “雪崩”?让你不仅会用 Executor,还能用得稳、用得好,成为并发领域的老手!

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值