一道看似简单的老题,背后藏着性能优化、资源控制、架构思想。
🪪 起因:一次“内存暴涨”事故
还记得我刚转岗去新项目那阵子,遇到一个非常“积极”的功能同事:
他说后台要同时给几百个用户发送短信通知,于是写了个循环,每次都 new Thread(() -> sendSms()).start()。
刚上线的第一天,系统内存暴涨、CPU 飙高,短信也发送失败大半。
一看代码,我差点没背过去:居然直接一口气 new 几百个线程。
而这也正是今天这道面试题的核心所在。
❓ 面试题:Java 中的线程池怎么用?为什么不用 new Thread?
🧩 第一步:new Thread 的问题到底在哪?
直接用 new Thread() 虽然简单,但存在几个关键问题:
| 问题点 | 描述 |
|---|---|
| ✅ 无法复用 | 每次执行任务都要重新创建线程,频繁创建销毁线程开销极大。 |
| ✅ 不可控 | 无法限制线程数量,来多少任务就 new 多少线程,容易导致 OOM 或线程爆炸。 |
| ✅ 无监控机制 | 没法知道当前有多少线程在运行、排队。 |
一个典型的问题就是:
for (int i = 0; i < 1000; i++) { new Thread(() -> doTask()).start();}
看着简单,实则危险:线程创建成本高,JVM 的线程栈空间是有限的,轻则 CPU 拉满,重则内存溢出。
🧠 第二步:线程池的本质是什么?
线程池(ThreadPool)是 Java 提供的线程复用机制,它的目标:
- 控制线程的数量
- 降低线程频繁创建/销毁的资源开销
- 提供任务缓存队列、拒绝策略、监控等机制
核心类是:
java.util.concurrent.ThreadPoolExecutor
⚙️ 第三步:线程池的关键参数理解
new ThreadPoolExecutor(
corePoolSize, // 核心线程数
maximumPoolSize, // 最大线程数
keepAliveTime, // 空闲线程最大存活时间
unit, // 时间单位
workQueue, // 任务队列
threadFactory, // 线程工厂
handler // 拒绝策略)
这些参数的组合,决定了线程池的行为模型。
举个面试喜欢问的例子:
如果核心线程数是 10,最大线程数是 100,队列长度是 1000,会出现什么现象?
⏱️ 答案:
- 前 10 个任务直接执行(core)
- 11 ~ 1010 个进入队列(queue)
- 超过 1010,才尝试扩充线程到最大(max)
- 超过 1100,会触发拒绝策略
这就是线程池“分级处理任务”的设计理念。
✅ 第四步:实际项目中怎么用?
我们通常不会自己手写上面的构造器,而是用工具类 Executors 来快速构造:
ExecutorService executor = Executors.newFixedThreadPool(10);executor.submit(() -> doSomething());
⚠️ 但注意:Executors 工厂方法也不是完全推荐使用的。
比如:
Executors.newCachedThreadPool()
默认最大线程数是
Integer.MAX_VALUE,可能导致线程过多 —— 慎用!
💡 正确方式:推荐手动构造线程池
ThreadPoolExecutor pool = new ThreadPoolExecutor(10,20,60L,TimeUnit.SECONDS,new LinkedBlockingQueue<>(1000),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
这里我们设定了核心数量、最大线程数、队列容量和拒绝策略。这样:
- 线程数量有边界
- 队列可控,避免 OOM
- 拒绝策略可监控异常情况
👀 那么如何选择线程池参数?
我在项目中常用以下方法估算:
线程数 ≈ CPU 核心数 * (1 + 平均等待时间 / 计算时间)
这也是《阿里巴巴 Java 开发手册》中提到的经典经验公式。
举例:
- I/O 密集型任务(如访问外部服务):线程数要略多
- CPU 密集型任务(如压缩、加密):线程数与核心数相当
📌 面试建议总结
这道题看似是考你“API”,其实是考你是否:
- 理解线程池背后的动机和设计理念
- 知道怎么用线程池去解决实际问题
- 能举出自己真实使用过线程池的例子
📢 建议答题结构:
1. new Thread 的问题
2. 线程池的优势
3. ThreadPoolExecutor 参数解析
4. 结合自己项目谈谈线程池配置实践
有真实经验,讲起来才有说服力!