一、为什么要使用线程池?
不使用线程池直接创建线程会带来严重问题:
-
资源开销大:频繁创建/销毁线程消耗大量CPU和内存资源,线程创建本身是重量级操作
-
系统不稳定:无限制创建线程可能导致:
- 内存溢出(OOM):每个线程需分配栈空间(默认1MB)
- 上下文切换频繁:线程过多导致CPU在切换上消耗过多资源
- 系统崩溃:耗尽操作系统线程资源
-
线程池的核心价值:
- 复用线程:避免重复创建销毁的开销
- 控制并发:限制最大线程数,保护系统资源
- 任务排队:通过队列平滑处理突发流量
- 统一管理:监控、统计、优雅关闭等能力
二、为什么不推荐使用Executors内置线程池?
阿里巴巴Java开发手册明确强制规定:线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor方式。原因如下:
| Executors方法 | 风险点 | 具体问题 |
|---|---|---|
newFixedThreadPool / newSingleThreadExecutor | 队列无界 | 内部使用LinkedBlockingQueue(默认容量Integer.MAX_VALUE),任务堆积会导致OOM博客园 |
newCachedThreadPool | 线程数无界 | 最大线程数为Integer.MAX_VALUE,高并发下可能创建海量线程导致系统崩溃知乎 |
newScheduledThreadPool | 队列无界 | 同样使用无界队列,存在内存溢出风险 |
核心问题:Executors封装过度,隐藏了关键参数(如队列容量、拒绝策略),开发者无法感知资源风险,容易在生产环境引发事故
✅ 正确做法:显式使用ThreadPoolExecutor构造函数,明确指定所有参数:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // corePoolSize
10, // maximumPoolSize
60L, // keepAliveTime
TimeUnit.SECONDS, // unit
new LinkedBlockingQueue<>(100), // 有界队列!
new CustomThreadFactory(), // 可自定义线程名
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
三、线程池核心参数(ThreadPoolExecutor 7个参数)
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 空闲线程存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory,// 线程工厂
RejectedExecutionHandler handler // 拒绝策略
)
| 参数 | 说明 | 最佳实践 |
|---|---|---|
| corePoolSize | 核心线程数,即使空闲也会保留 | 根据CPU核心数和任务类型设置: • CPU密集型:N+1(N为CPU核心数) • IO密集型:2N或更高阿里云官方网站 |
| maximumPoolSize | 最大线程数,队列满后可扩容至此值 | 需结合队列容量设置,避免无界增长 |
| keepAliveTime | 非核心线程空闲存活时间 | 通常设为30-60秒,平衡资源回收与创建开销 |
| workQueue | 任务队列 | 必须使用有界队列(如ArrayBlockingQueue),避免OOM知乎 |
| threadFactory | 线程工厂 | 自定义线程名(如"order-task-pool-%d"),便于排查问题 |
| handler | 拒绝策略 | 根据业务场景选择(见下文) |
执行流程:
- 当前运行线程数 <
corePoolSize→ 创建新核心线程执行 - 达到
corePoolSize→ 任务入队workQueue - 队列满且线程数 <
maximumPoolSize→ 创建非核心线程 - 队列满且达到
maximumPoolSize→ 触发拒绝策略
四、线程池拒绝策略(4种内置 + 自定义)
当线程数达到maximumPoolSize + 队列已满时触发拒绝策略:
| 策略 | 行为 | 适用场景 |
|---|---|---|
| AbortPolicy(默认) | 抛出RejectedExecutionException异常 | 通用场景,快速失败便于发现问题腾讯 |
| CallerRunsPolicy | 由提交任务的线程(调用者)直接执行 | 降低提交速度,适用于允许降级的场景 |
| DiscardPolicy | 静默丢弃任务,不抛异常 | 允许任务丢失的场景(如日志上报) |
| DiscardOldestPolicy | 丢弃队列中最老的任务,尝试重新提交新任务 | 保留最新任务的场景(如实时数据处理) |
自定义拒绝策略示例:
public class CustomRejectPolicy implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 1. 记录日志告警
log.warn("Task rejected, queue size: {}", executor.getQueue().size());
// 2. 可选:持久化到DB/消息队列,后续重试
// taskPersistence.save(r);
// 3. 可选:降级处理
// fallbackService.handle(r);
throw new RejectedExecutionException("Task rejected due to overload");
}
}
五、生产环境最佳实践
-
必须使用有界队列:
new ArrayBlockingQueue<>(100),避免OOM -
监控关键指标:
executor.getActiveCount(); // 活跃线程数 executor.getQueue().size(); // 队列堆积量 executor.getCompletedTaskCount();// 完成任务数 -
优雅关闭:
executor.shutdown(); // 停止接收新任务,等待已有任务完成 // 或 executor.shutdownNow(); // 立即中断所有任务 -
命名规范:通过
ThreadFactory设置有意义的线程名,便于排查问题 -
参数调优:根据压测结果动态调整
corePoolSize/queueCapacity知乎
💡 总结:线程池是双刃剑——用得好提升系统吞吐量,用不好直接导致生产事故。务必显式创建
ThreadPoolExecutor,明确所有参数,尤其是队列必须有界,并根据业务选择合适的拒绝策略。
一、线程池处理任务的完整流程
ThreadPoolExecutor 的任务处理遵循以下四步决策链:
关键细节:
-
核心线程默认不会超时回收(除非调用
allowCoreThreadTimeOut(true)) -
非核心线程在空闲超过
keepAliveTime后会被回收 -
队列类型影响行为:
SynchronousQueue:不存储任务,必须立即有空闲线程,否则创建新线程(适合CachedThreadPool)LinkedBlockingQueue/ArrayBlockingQueue:先入队,队列满才扩容线程
二、线程异常后:销毁还是复用?
✅ 线程会被复用,但需正确处理异常,否则可能“静默死亡”导致线程池失效。
问题场景(错误写法):
executor.execute(() -> {
throw new RuntimeException("任务异常"); // 未捕获 → 线程终止,任务丢失
});
此线程会因未捕获异常而退出,线程池会创建新线程替代,但任务已丢失且无日志,极难排查。
正确做法(3种方案):
方案1:任务内部 try-catch(推荐)
executor.execute(() -> {
try {
// 业务逻辑
process();
} catch (Exception e) {
log.error("任务执行异常", e);
// 可选:告警、降级、重试
}
});
方案2:自定义 ThreadFactory 捕获未处理异常
ThreadFactory namedFactory = r -> {
Thread t = new Thread(r, "biz-task-" + seq.incrementAndGet());
t.setUncaughtExceptionHandler((thread, ex) -> {
log.error("线程[{}]未捕获异常", thread.getName(), ex);
monitor.alert("ThreadPoolUncaughtException", ex);
});
return t;
};
方案3:继承 ThreadPoolExecutor 重写 afterExecute
public class SafeThreadPool extends ThreadPoolExecutor {
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
if (t == null && r instanceof Future<?>) {
try {
((Future<?>) r).get(); // 获取异步任务异常
} catch (CancellationException ce) {
t = ce;
} catch (ExecutionException ee) {
t = ee.getCause();
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
if (t != null) {
log.error("任务执行异常", t);
}
}
}
✅ 结论:线程异常不会自动销毁整个线程池,但未捕获异常会导致该线程退出,线程池会创建新线程补充。务必通过上述方式捕获异常,避免任务静默失败。
三、如何给线程命名?
使用 ThreadFactory 自定义线程名,便于日志追踪和问题定位:
public class NamedThreadFactory implements ThreadFactory {
private final AtomicInteger seq = new AtomicInteger(1);
private final String namePrefix;
private final boolean daemon;
public NamedThreadFactory(String namePrefix, boolean daemon) {
this.namePrefix = namePrefix;
this.daemon = daemon;
}
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, namePrefix + "-" + seq.getAndIncrement());
t.setDaemon(daemon);
t.setUncaughtExceptionHandler((thread, ex) ->
log.error("线程[{}]异常退出", thread.getName(), ex)
);
return t;
}
}
// 使用
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, 10, 60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
new NamedThreadFactory("order-service", false),
new ThreadPoolExecutor.CallerRunsPolicy()
);
✅ 命名规范建议:{业务模块}-{功能}-{序号},如 pay-async-01、user-query-03
四、如何动态修改线程池参数?
ThreadPoolExecutor 提供线程安全的 setter 方法,支持运行时调参:
ThreadPoolExecutor executor = new ThreadPoolExecutor(...);
// 动态调整核心线程数
executor.setCorePoolSize(8);
// 动态调整最大线程数
executor.setMaximumPoolSize(20);
// 动态调整空闲超时时间
executor.setKeepAliveTime(30L, TimeUnit.SECONDS);
// 允许核心线程超时回收(默认false)
executor.allowCoreThreadTimeOut(true);
⚠️ 注意事项:
| 参数 | 调整方向 | 影响 |
|---|---|---|
corePoolSize 增大 | 立即生效 | 可能立即创建新线程 |
corePoolSize 减小 | 延迟生效 | 现有核心线程不会立即销毁,需等空闲超时 |
maximumPoolSize | 立即生效 | 影响后续扩容上限 |
keepAliveTime | 立即生效 | 影响后续空闲线程回收 |