线程池
线程池用于管理和复用线程,是 Java 服务端并发编程的核心工具。它能限制线程数量、复用工作线程、缓冲任务、统一处理拒绝和关闭。
生产系统中,几乎不应该为每个请求直接 new Thread。
# 1. 为什么需要线程池
线程池解决:
| 问题 | 说明 |
|---|---|
| 创建销毁成本 | 复用线程,减少频繁创建 |
| 资源失控 | 限制线程数量,保护系统 |
| 任务排队 | 用队列缓冲短时峰值 |
| 统一治理 | 命名、监控、拒绝、关闭 |
| 异步执行 | 主流程快速返回,任务后台执行 |
模型:
提交任务
│
▼
ThreadPoolExecutor
│
├─ 工作线程
├─ 任务队列
├─ 线程工厂
└─ 拒绝策略
# 2. 核心参数
new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
threadFactory,
rejectedExecutionHandler
);
| 参数 | 含义 |
|---|---|
corePoolSize | 核心线程数 |
maximumPoolSize | 最大线程数 |
keepAliveTime | 非核心线程空闲存活时间 |
workQueue | 任务队列 |
threadFactory | 创建线程 |
handler | 拒绝策略 |
# 3. 执行流程
execute(task)
│
├─ 当前线程数 < corePoolSize
│ └─ 新建核心线程执行
│
├─ 否则尝试入队
│ └─ 入队成功,等待执行
│
├─ 队列满且线程数 < maximumPoolSize
│ └─ 新建非核心线程执行
│
└─ 仍无法处理
└─ 执行拒绝策略
关键点:如果队列是无界队列,任务会一直入队,maximumPoolSize 可能永远用不上。
# 4. 工作队列
| 队列 | 特点 | 风险 |
|---|---|---|
ArrayBlockingQueue | 有界数组队列 | 需要指定容量 |
LinkedBlockingQueue | 链表队列,可有界 | 默认容量很大,要显式设置 |
SynchronousQueue | 不存储任务,直接交接 | 容易创建大量线程 |
PriorityBlockingQueue | 优先级队列 | 无界风险,顺序由比较器决定 |
DelayQueue | 延迟队列 | 适合定时/延迟任务 |
生产环境优先使用有界队列,避免任务无限堆积导致 OOM。
# 5. 拒绝策略
| 策略 | 行为 |
|---|---|
AbortPolicy | 抛 RejectedExecutionException |
CallerRunsPolicy | 调用者线程执行任务 |
DiscardPolicy | 静默丢弃新任务 |
DiscardOldestPolicy | 丢弃队列最老任务,再尝试提交 |
推荐:
- 默认可以用
AbortPolicy,让问题暴露。 - 限流削峰可考虑
CallerRunsPolicy,让提交方变慢。 - 不要随意使用静默丢弃,除非业务允许。
# 6. 线程工厂
线程池必须命名。
ThreadFactory factory = new ThreadFactory() {
private final AtomicInteger index = new AtomicInteger();
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("order-worker-" + index.incrementAndGet());
thread.setUncaughtExceptionHandler((t, e) -> log.error("线程异常: {}", t.getName(), e));
return thread;
}
};
线程名能直接提升排查效率。
# 7. 不推荐 Executors 快捷方法
Executors 很方便,但隐藏了队列和线程数风险:
| 方法 | 风险 |
|---|---|
newFixedThreadPool | 默认无界队列,任务可能堆积 |
newSingleThreadExecutor | 默认无界队列 |
newCachedThreadPool | 最大线程数很大,可能创建过多线程 |
newScheduledThreadPool | 延迟队列任务堆积风险 |
生产代码推荐显式创建 ThreadPoolExecutor,写清线程数、队列容量和拒绝策略。
# 8. 线程数估算
CPU 密集型:
线程数 ≈ CPU 核心数 或 核心数 + 1
IO 密集型:
线程数 ≈ CPU 核心数 * (1 + 等待时间 / 计算时间)
这只是估算,最终要压测。
观察指标:
- 活跃线程数。
- 队列长度。
- 拒绝次数。
- 任务耗时。
- CPU 使用率。
- 下游连接池使用率。
# 9. 关闭线程池
优雅关闭:
executor.shutdown();
if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
区别:
| 方法 | 行为 |
|---|---|
shutdown | 不接收新任务,继续执行队列任务 |
shutdownNow | 尝试中断正在执行任务,返回未执行任务 |
任务要响应中断,否则 shutdownNow 也不能保证立即停止。
# 10. 异常处理
execute 中异常会抛给线程异常处理器。submit 中异常会封装到 Future。
Future<?> future = executor.submit(() -> {
throw new RuntimeException("error");
});
future.get(); // 才能观察到异常
建议:
- 任务内部记录关键异常。
- 对 Future 结果做消费。
- 使用
afterExecute统一处理。 - 监控线程池拒绝和异常。
# 11. 常见配置模板
ThreadPoolExecutor executor = new ThreadPoolExecutor(
8,
16,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1000),
factory,
new ThreadPoolExecutor.CallerRunsPolicy()
);
这不是万能参数,只是说明生产线程池应有边界。
# Tips 快问快答
Q:线程池核心参数有哪些? A:核心线程数、最大线程数、存活时间、队列、线程工厂、拒绝策略。
Q:线程池提交任务流程是什么? A:先建核心线程,再入队,队列满再建非核心线程,最后拒绝。
Q:为什么不推荐 Executors? A:它隐藏无界队列或过大线程数风险。
Q:maximumPoolSize 什么时候生效? A:队列满且当前线程数小于最大线程数时。
Q:生产线程池为什么要有界队列? A:防止任务无限堆积导致内存溢出。
Q:CallerRunsPolicy 有什么效果? A:由提交线程执行任务,能对上游形成反压。
Q:线程池怎么优雅关闭?
A:先 shutdown,等待超时后必要时 shutdownNow。
Q:submit 的异常去哪了?
A:封装在 Future 中,调用 get 时抛出。