摘要:从一次"创建10万个线程导致服务器宕机"的乌龙事件出发,深度剖析线程池的核心原理与最佳实践。通过手写50行代码实现简易版线程池、7个核心参数的真实案例、以及4种拒绝策略的选型对比,揭秘为什么线程不是越多越好、如何设置线程池大小、以及ThreadPoolExecutor的工作流程。配合时序图展示任务提交流程,给出线程池使用的10条军规。
💥 翻车现场
周二下午,哈吉米写了一个"批量导入用户"的功能。
// 导入10万个用户
public void importUsers(List<User> users) {
for (User user : users) {
// 每个用户创建一个线程处理
new Thread(() -> {
saveUser(user);
}).start();
}
}
哈吉米:"多线程并发处理,应该很快!"
点击"导入"按钮后……
5秒后:
服务器CPU:100%
内存:爆满
应用:卡死
错误日志:
OutOfMemoryError: unable to create new native thread
哈吉米:"卧槽,服务器宕机了!"
紧急重启后,南北绿豆和阿西噶阿西赶来了。
南北绿豆:"你创建了10万个线程?服务器不宕机才怪!"
哈吉米:"线程不是越多越好吗?"
阿西噶阿西:"大错特错!线程是有开销的,而且操作系统能支持的线程数有限!"
南北绿豆:"来,我给你讲讲为什么需要线程池。"
🤔 为什么需要线程池?
问题1:线程创建和销毁的开销
// 每次创建新线程
new Thread(() -> {
saveUser(user); // 执行0.1秒
}).start();
// 开销:
// 创建线程:1ms
// 执行任务:0.1秒
// 销毁线程:1ms
// 总耗时:0.1秒 + 2ms
// 如果创建10万个线程:
// 创建+销毁:10万 × 2ms = 200秒
// 任务执行:10万 × 0.1秒 = 10000秒
// 总耗时:10200秒
// 用线程池(复用线程):
// 创建线程:100个 × 1ms = 0.1秒
// 执行任务:10万 × 0.1秒 = 10000秒(并发100个,实际100秒)
// 总耗时:100秒
性能提升:100倍
阿西噶阿西:"线程池通过复用线程,避免频繁创建和销毁。"
问题2:线程数量失控
操作系统限制:
- Linux默认:单个进程最多创建1024个线程
- Windows:受内存限制
创建10万个线程:
- 每个线程栈空间:1MB
- 总内存:10万 × 1MB = 100GB
- 结果:OutOfMemoryError
南北绿豆:"线程池可以限制线程数量,防止资源耗尽。"
问题3:无法管理和监控
// 创建线程后,无法知道:
// - 有多少线程在运行?
// - 有多少任务在等待?
// - 任务执行成功还是失败?
new Thread(() -> {
saveUser(user);
}).start(); // 启动后就不管了
南北绿豆:"线程池提供了管理和监控能力。"
🎯 线程池的核心原理
ThreadPoolExecutor的结构
public class ThreadPoolExecutor {
// 核心组件
private final BlockingQueue<Runnable> workQueue; // 任务队列
private final HashSet<Worker> workers; // 工作线程集合
private volatile int corePoolSize; // 核心线程数
private volatile int maximumPoolSize; // 最大线程数
private volatile long keepAliveTime; // 空闲线程存活时间
private volatile RejectedExecutionHandler handler;// 拒绝策略
// Worker(工作线程)
private final class Worker implements Runnable {
final Thread thread;
Runnable firstTask;
public void run() {
while (task != null || (task = getTask()) != null) {
task.run(); // 执行任务
}
}
}
}
线程池的工作流程
graph TD
A[提交任务] --> B{当前线程数 < corePoolSize?}
B -->|是| C[创建核心线程执行]
B -->|否| D{任务队列是否满?}
D -->|否| E[任务加入队列]
D -->|是| F{当前线程数 < maximumPoolSize?}
F -->|是| G[创建非核心线程执行]
F -->|否| H[执行拒绝策略]
C --> I[任务执行]
E --> J[空闲线程从队列取任务]
G --> I
J --> I
style C fill:#90EE90
style E fill:#ADD8E6
style G fill:#FFE4B5
style H fill:#FFB6C1
关键流程:
1. 线程数 < corePoolSize → 创建核心线程
2. 线程数 >= corePoolSize → 任务加入队列
3. 队列满了 + 线程数 < maximumPoolSize → 创建非核心线程
4. 队列满了 + 线程数 >= maximumPoolSize → 拒绝任务
时序图演示
sequenceDiagram
participant Client as 客户端
participant Pool as 线程池
participant Queue as 任务队列
participant CoreThread as 核心线程
participant NonCoreThread as 非核心线程
Note over Pool: corePoolSize=2, maxPoolSize=5, queueSize=3
Client->>Pool: 1. 提交任务1
Pool->>CoreThread: 创建核心线程1 ✅
Client->>Pool: 2. 提交任务2
Pool->>CoreThread: 创建核心线程2 ✅
Client->>Pool: 3. 提交任务3
Pool->>Queue: 加入队列[task3] ✅
Client->>Pool: 4. 提交任务4
Pool->>Queue: 加入队列[task3, task4] ✅
Client->>Pool: 5. 提交任务5
Pool->>Queue: 加入队列[task3, task4, task5] ✅
Client->>Pool: 6. 提交任务6
Note over Pool: 队列满了,创建非核心线程
Pool->>NonCoreThread: 创建非核心线程3 ✅
Client->>Pool: 7. 提交任务7
Pool->>NonCoreThread: 创建非核心线程4 ✅
Client->>Pool: 8. 提交任务8
Pool->>NonCoreThread: 创建非核心线程5 ✅
Client->>Pool: 9. 提交任务9
Note over Pool: 队列满 + 线程数达到max
Pool->>Client: 拒绝任务 ❌
🎯 7个核心参数详解
参数详解
ThreadPoolExecutor executor = new ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 空闲线程存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
);
参数1:corePoolSize(核心线程数)
定义:线程池维持的最少线程数,即使空闲也不会销毁。
示例:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // corePoolSize=5
...
);
// 提交3个任务 → 创建3个核心线程
// 任务执行完 → 3个核心线程保持活跃(不销毁)
如何设置?
CPU密集型任务:
corePoolSize = CPU核心数 + 1
IO密集型任务:
corePoolSize = CPU核心数 × 2
示例:8核CPU
- CPU密集:corePoolSize = 9
- IO密集:corePoolSize = 16
参数2:maximumPoolSize(最大线程数)
定义:线程池允许创建的最大线程数。
何时创建?
只有当:
1. 任务队列满了
2. 当前线程数 < maximumPoolSize
才会创建非核心线程
示例:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // corePoolSize=2
5, // maximumPoolSize=5(最多5个线程)
60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3) // 队列容量3
);
// 提交6个任务:
// 任务1、2 → 创建核心线程1、2
// 任务3、4、5 → 加入队列[3, 4, 5]
// 任务6 → 队列满,创建非核心线程3
// 提交第7、8个任务 → 创建非核心线程4、5
// 提交第9个任务 → 拒绝(线程数=5,队列满)
参数3:keepAliveTime(空闲存活时间)
定义:非核心线程空闲多久后被销毁。
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, 5,
60L, TimeUnit.SECONDS, // 非核心线程空闲60秒后销毁
...
);
// 高峰期:5个线程都在工作
// 低峰期:任务减少,非核心线程空闲60秒后自动销毁,剩下2个核心线程
阿西噶阿西:"这样可以动态伸缩,高峰期多线程,低峰期少线程。"
参数4:workQueue(任务队列)
3种常用队列:
| 队列类型 | 容量 | 特点 | 适用场景 |
|---|---|---|---|
| ArrayBlockingQueue | 有界 | 数组实现,先进先出 | 资源可控 |
| LinkedBlockingQueue | 可选有界/无界 | 链表实现 | ⭐⭐⭐⭐⭐ 常用 |
| SynchronousQueue | 0 | 不存储任务,直接交给线程 | 任务立即执行 |
示例:
// 有界队列(推荐)
new ArrayBlockingQueue<>(1000); // 最多1000个任务
// 无界队列(危险)
new LinkedBlockingQueue<>(); // 无限大,可能OOM
// 同步队列
new SynchronousQueue<>(); // 不缓存任务,直接给线程执行
参数5:threadFactory(线程工厂)
作用:自定义线程的创建方式。
ThreadFactory factory = new ThreadFactory() {
private final AtomicInteger threadNumber = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("my-pool-thread-" + threadNumber.getAndIncrement());
t.setDaemon(false); // 非守护线程
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
};
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, 10, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100),
factory // 自定义线程工厂
);
好处:
- ✅ 线程有意义的名称(方便排查问题)
- ✅ 可以设置优先级、守护线程等
参数6-7:拒绝策略
4种拒绝策略:
| 策略 | 行为 | 适用场景 |
|---|---|---|
| AbortPolicy(默认) | 抛出异常 | 需要感知拒绝 |
| CallerRunsPolicy | 调用者线程执行 | 重要任务,不能丢 |
| DiscardPolicy | 静默丢弃 | 不重要的任务 |
| DiscardOldestPolicy | 丢弃队列头的任务 | 保留最新任务 |
示例:
// AbortPolicy(默认)
executor.execute(task);
// 拒绝时抛异常:RejectedExecutionException
// CallerRunsPolicy
new ThreadPoolExecutor(
2, 5, 60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3),
new ThreadPoolExecutor.CallerRunsPolicy() // 调用者执行
);
// 效果:
executor.execute(task); // 队列满了
// 当前线程(main线程)执行task
🎯 手写一个简易版线程池
哈吉米:"能不能自己实现一个线程池?"
南北绿豆:"来,50行代码实现核心功能!"
/**
* 简易版线程池(理解原理)
*/
public class SimpleThreadPool {
// 任务队列
private final BlockingQueue<Runnable> workQueue;
// 工作线程列表
private final List<WorkerThread> workers = new ArrayList<>();
// 线程池大小
private final int poolSize;
// 是否关闭
private volatile boolean isShutdown = false;
public SimpleThreadPool(int poolSize, int queueSize) {
this.poolSize = poolSize;
this.workQueue = new ArrayBlockingQueue<>(queueSize);
// 创建工作线程
for (int i = 0; i < poolSize; i++) {
WorkerThread worker = new WorkerThread("worker-" + i);
workers.add(worker);
worker.start();
}
}
/**
* 提交任务
*/
public void execute(Runnable task) {
if (isShutdown) {
throw new IllegalStateException("线程池已关闭");
}
// 加入队列(阻塞,队列满时等待)
try {
workQueue.put(task);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
/**
* 关闭线程池
*/
public void shutdown() {
isShutdown = true;
// 中断所有工作线程
for (WorkerThread worker : workers) {
worker.interrupt();
}
}
/**
* 工作线程
*/
private class WorkerThread extends Thread {
public WorkerThread(String name) {
super(name);
}
@Override
public void run() {
while (!isShutdown) {
try {
// 从队列取任务(阻塞,队列空时等待)
Runnable task = workQueue.take();
// 执行任务
task.run();
} catch (InterruptedException e) {
// 线程池关闭,退出循环
break;
}
}
}
}
}
测试代码
public class SimpleThreadPoolTest {
public static void main(String[] args) {
// 创建线程池:5个线程,队列容量10
SimpleThreadPool pool = new SimpleThreadPool(5, 10);
// 提交20个任务
for (int i = 0; i < 20; i++) {
final int taskId = i;
pool.execute(() -> {
System.out.println("执行任务" + taskId + ",线程:" + Thread.currentThread().getName());
try {
Thread.sleep(1000); // 模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 等待5秒后关闭
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
pool.shutdown();
}
}
输出:
执行任务0,线程:worker-0
执行任务1,线程:worker-1
执行任务2,线程:worker-2
执行任务3,线程:worker-3
执行任务4,线程:worker-4
执行任务5,线程:worker-0 ← 复用了worker-0
执行任务6,线程:worker-1 ← 复用了worker-1
...
哈吉米:"原来线程池就是:固定数量的线程 + 任务队列 + 不断取任务执行!"
🎯 如何设置线程池大小?
CPU密集型任务
CPU密集型:大量计算,少量IO
示例:
- 加密解密
- 数据压缩
- 图像处理
推荐线程数:
线程数 = CPU核心数 + 1
原因:CPU密集型,线程太多会频繁上下文切换,反而变慢
测试:
| 线程数 | 执行时间 |
|---|---|
| 4(CPU核心数) | 10秒 |
| 8 | 10.5秒 |
| 16 | 12秒 |
| 32 | 15秒(上下文切换开销) |
IO密集型任务
IO密集型:大量IO操作,少量计算
示例:
- 数据库查询
- HTTP请求
- 文件读写
推荐线程数:
线程数 = CPU核心数 × 2(或更多)
原因:IO等待时,线程阻塞,CPU空闲,可以增加线程数
更精确的公式:
线程数 = CPU核心数 × (1 + IO耗时 / CPU耗时)
示例:
- CPU核心数:8
- IO耗时:80ms
- CPU耗时:20ms
线程数 = 8 × (1 + 80 / 20) = 8 × 5 = 40
混合型任务
拆分成CPU密集型和IO密集型,用不同线程池处理
示例:
cpuPool = new ThreadPoolExecutor(9, 9, ...); // CPU密集
ioPool = new ThreadPoolExecutor(40, 40, ...); // IO密集
🛡️ 线程池使用的10条军规
军规1:不要用Executors创建线程池
// ❌ 错误(OOM风险)
ExecutorService executor = Executors.newFixedThreadPool(10);
// 底层实现:
new ThreadPoolExecutor(10, 10, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()); // 无界队列,可能OOM
// ✅ 正确
ThreadPoolExecutor executor = new ThreadPoolExecutor(
10, 20, 60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1000), // 有界队列
new ThreadPoolExecutor.CallerRunsPolicy()
);
原因:Executors创建的线程池队列是无界的,可能OOM。
军规2:合理设置队列大小
// ❌ 队列太小(频繁拒绝)
new ArrayBlockingQueue<>(10);
// ❌ 队列太大(OOM)
new ArrayBlockingQueue<>(1000000);
// ✅ 合理大小
new ArrayBlockingQueue<>(1000);
军规3:必须设置拒绝策略
// ✅ 推荐CallerRunsPolicy
new ThreadPoolExecutor.CallerRunsPolicy();
// 效果:任务由调用者线程执行,自然降速
军规4:给线程池起有意义的名字
ThreadFactory factory = new ThreadFactoryBuilder()
.setNameFormat("order-pool-%d")
.build();
军规5:监控线程池状态
// 定期打印线程池状态
@Scheduled(fixedDelay = 10000)
public void monitorThreadPool() {
log.info("活跃线程数: {}", executor.getActiveCount());
log.info("队列任务数: {}", executor.getQueue().size());
log.info("已完成任务数: {}", executor.getCompletedTaskCount());
}
军规6:任务要捕获异常
// ❌ 错误
executor.execute(() -> {
processOrder(order); // 可能抛异常,导致线程挂掉
});
// ✅ 正确
executor.execute(() -> {
try {
processOrder(order);
} catch (Exception e) {
log.error("任务执行失败", e);
}
});
军规7-10(快速总结)
- 不要在锁内提交任务(可能死锁)
- 及时关闭线程池(shutdown)
- 区分CPU密集和IO密集任务
- 压测验证线程池参数
🎓 面试标准答案
题目:线程池的工作原理是什么?
答案:
核心组件:
- 核心线程(corePoolSize)
- 任务队列(workQueue)
- 最大线程(maximumPoolSize)
- 拒绝策略(handler)
工作流程:
- 线程数 < corePoolSize → 创建核心线程
- 线程数 >= corePoolSize → 任务加入队列
- 队列满 + 线程数 < maxPoolSize → 创建非核心线程
- 队列满 + 线程数 >= maxPoolSize → 拒绝任务
线程池大小设置:
- CPU密集:CPU核心数 + 1
- IO密集:CPU核心数 × 2
🎉 结束语
晚上10点,哈吉米把代码改成了线程池。
哈吉米:"用线程池后,导入10万用户从300秒降到100秒,而且不会OOM了!"
南北绿豆:"对,线程池复用线程,避免频繁创建销毁。"
阿西噶阿西:"记住:不要直接new Thread,用线程池管理线程。"
哈吉米:"还有线程数不是越多越好,要根据任务类型设置!"
南北绿豆:"对,CPU密集型少线程,IO密集型多线程!"
记忆口诀:
线程池复用线程快,核心参数要记牢
core先创max后到,队列满了才扩容
CPU密集核心数,IO密集翻一倍
拒绝策略要设置,监控状态别忘了
别用Executors有风险,手动创建才安全
希望这篇文章能帮你轻松理解线程池的原理!记住:理解了线程池,就理解了并发编程的核心思想!💪