线程池详解
线程池(ThreadPoolExecutor)通过复用线程、队列化任务、限制并发度来提升吞吐并避免频繁创建线程的开销,是服务端并发的“必备基础设施”。
为什么需要线程池?
- 创建销毁成本高:频繁
new Thread()会触发内核调度与内存分配。 - 资源可控:线程数不可无限增长,否则会触发 OOM 或导致上下文切换风暴。
- 任务管理:线程池能统一应用链路的超时、降级、监控、优雅关闭。
- 异步编排:配合
Future,CompletableFuture构建异步流水线。
ThreadPoolExecutor 核心参数
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
| 参数 | 作用 | 设计建议 |
|---|---|---|
corePoolSize | 常驻线程数 | IO 密集型可设置为 CPU * 2,或按压测结果调优 |
maximumPoolSize | 最大线程数 | 防止突发流量压垮系统,可配合熔断限流 |
workQueue | 任务队列 | 有界队列可提供背压;SynchronousQueue 适合直接移交 |
keepAliveTime | 非核心线程闲置回收时间 | IO 场景可略高,CPU 场景可更短 |
threadFactory | 设定线程名、优先级、是否守护线程 | 便于排查,推荐自定义命名 |
handler | 拒绝策略 | 结合业务场景选择或自定义 |
常用队列:
LinkedBlockingQueue(默认无界,谨慎)、ArrayBlockingQueue(有界)、SynchronousQueue(直通,高并发下触发扩容)。
任务调度流程
- 线程数 <
corePoolSize:直接创建新线程执行任务; - 否则尝试入队
workQueue; - 队列满且线程数 <
maximumPoolSize:再创建非核心线程; - 再次失败则触发拒绝策略。
拒绝策略
AbortPolicy:抛RejectedExecutionException,默认策略。CallerRunsPolicy:调用者线程直接执行任务,提供自然的背压。DiscardPolicy:悄然丢弃,需谨慎。DiscardOldestPolicy:丢弃队头任务再尝试入队,可能导致任务饥饿。- 自定义策略:记录日志、降级响应、写入 MQ 重试等。
Executors 快捷工厂的坑
| 工厂方法 | 问题 | 建议替代 |
|---|---|---|
newFixedThreadPool | 使用无界 LinkedBlockingQueue,任务堆积易 OOM | 显式 new ThreadPoolExecutor |
newCachedThreadPool | 最大线程数 Integer.MAX_VALUE,高峰可能撑爆 | 设定合理 maximumPoolSize |
newScheduledThreadPool | 核心线程无上限回收,提交过多一次性任务会堆积 | JDK9+ 用 ScheduledThreadPoolExecutor + 自定义队列 |
newSingleThreadExecutor | 单线程一旦被阻塞,所有任务等待 | 必要时用 ThreadPoolExecutor + 有界队列 |
面试常问:“为什么不建议直接使用
Executors?”——答:参数不可控,隐藏 OOM 风险。
自定义线程池示例
ThreadPoolExecutor bizPool = new ThreadPoolExecutor(
8, // core
16, // max
60, TimeUnit.SECONDS, // keepAlive
new ArrayBlockingQueue<>(2000), // 有界队列
new ThreadFactoryBuilder()
.setNameFormat("biz-worker-%d")
.setUncaughtExceptionHandler((t, e) -> log.error("线程崩溃", e))
.build(),
new ThreadPoolExecutor.CallerRunsPolicy() // 背压
);
// 优雅关闭
bizPool.shutdown();
if (!bizPool.awaitTermination(30, TimeUnit.SECONDS)) {
bizPool.shutdownNow();
}
监控与调优指标
- 线程池运行状态:
getPoolSize,getActiveCount,getTaskCount,getCompletedTaskCount。 - 队列长度:
workQueue.size(),是观察积压的关键指标。 - 拒绝次数:自定义
RejectedExecutionHandler统计。 - 任务耗时:在提交任务前后打点或使用
TimingThreadPool装饰器。 - 链路超时:线程池只是执行体,仍需配合业务超时控制(
Future#get(timeout))。
线程池拆分策略
- 按业务隔离:接口调用、异步 MQ、批任务各用一个线程池,避免互相拖垮。
- 按 SLA 级别:核心链路线程池优先保留资源,低优任务可使用更小的池或降级。
- 按请求类型:CPU 密集型与 IO 密集型配置不同,避免混用。
- 按租户/用户:多租户系统可考虑限流 + 线程池隔离。
常见问题排查
- CPU 打满:检查是否提交了大量 CPU 计算任务,可通过
jstack看活跃线程栈帧。 - 任务堆积:观察队列长度与
activeCount,必要时扩容或开启降级逻辑。 - 线程泄漏:忘记调用
shutdown,或线程池被频繁创建导致占满资源。 - 拒绝策略频繁触发:说明系统已超容量,需扩容、限流或快速失败。
高频面试题
-
如何估算线程池大小?
CPU 密集型:CPU 核心数 + 1;IO 密集型:CPU 核心数 * (1 + 平均等待时间/平均计算时间);最终以压测数据为准。 -
如何实现优先级线程池?
使用PriorityBlockingQueue作为workQueue,任务实现Comparable或自定义Comparator。 -
如何动态调整线程池参数?
调用setCorePoolSize,setMaximumPoolSize,setKeepAliveTime即可,可结合配置中心热更新。 -
shutdown()与shutdownNow()区别?
shutdown()进入平滑关闭状态,不再接收新任务但会完成已提交任务;shutdownNow()尝试中断正在执行的任务并清空队列。 -
如何避免线程池中的任务丢失?
- 任务入队失败时记录日志 / 写入补偿队列
- 使用可靠消息(MQ)或持久化
- 应用关闭前
awaitTermination,确保处理完成
最佳实践清单
- 明确线程池用途并按业务拆分。
- 选择有界队列并评估容量。
- 自定义
ThreadFactory,设置易读的线程名。 - 结合监控平台暴露线程池指标。
- 制定拒绝策略:重试、降级或快速失败。
- 定期复盘压测数据,动态调参。
小结
- 线程池核心在于“控制与复用”:控制线程数量、任务堆积与失败策略,复用线程提升吞吐。
- 切勿直接使用
Executors默认工厂,显式构造可读、可控的ThreadPoolExecutor。 - 监控 + 限流 + 降级是线程池稳定运行的三件套。