深入理解 Java 线程池:参数、拒绝策略与常见问题
一、线程池的核心参数
Java 线程池的核心实现是 ThreadPoolExecutor
,其构造函数包含以下关键参数:
java
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
)
-
核心线程数(
corePoolSize
)线程池中常驻的线程数量,即使空闲也不会被销毁(除非设置
allowCoreThreadTimeOut
)。 -
最大线程数(
maximumPoolSize
)线程池允许创建的最大线程数。当任务队列满时,线程池会尝试创建新线程,直到达到此上限。
-
存活时间(
keepAliveTime
)非核心线程的空闲存活时间。超过此时间且无新任务时,非核心线程会被销毁。
-
时间单位(
unit
)存活时间的单位(如
TimeUnit.SECONDS
)。 -
任务队列(
workQueue
)用于缓存未执行任务的阻塞队列,常见类型:
- 有界队列 :如
ArrayBlockingQueue
,需指定容量,任务超出容量后会触发线程扩容。 - 无界队列 :如
LinkedBlockingQueue
(默认无界),可能导致 OOM。 - 同步移交队列 :如
SynchronousQueue
,不存储任务,直接提交给线程。
- 有界队列 :如
-
线程工厂(
threadFactory
)用于创建线程,可自定义线程名称、优先级等(如通过
ThreadFactoryBuilder
)。 -
拒绝策略(
handler
)当线程池和队列均满时,处理新提交任务的策略(详见第二部分)。
二、线程池的拒绝策略详解
当线程池达到最大线程数且队列已满时,新提交的任务会触发拒绝策略。Java 提供了四种内置策略:
-
AbortPolicy(默认策略)
- 行为 :直接抛出
RejectedExecutionException
。 - 适用场景:严格要求任务不丢失的场景(如支付交易),需上层代码捕获异常处理。
- 行为 :直接抛出
-
CallerRunsPolicy
- 行为:由提交任务的线程直接执行该任务。
- 适用场景:异步转同步的降级策略,但可能阻塞主线程(如 Web 服务的请求线程)。
-
DiscardPolicy
- 行为:静默丢弃新任务,不抛异常也不执行。
- 适用场景:允许任务丢失的场景(如日志采集)。
-
DiscardOldestPolicy
- 行为:丢弃队列中最旧的任务(即队列头部的任务),然后重新提交新任务。
- 风险:可能丢失关键任务,需确保队列中的任务可丢弃。
自定义拒绝策略示例
可通过实现 RejectedExecutionHandler
接口自定义策略,例如记录日志或持久化任务:
java
new ThreadPoolExecutor.AbortPolicy() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 记录任务信息
log.error("Task rejected: {}", r);
// 可选:将任务持久化到数据库或消息队列
saveToDB(r);
// 抛出异常或降级处理
throw new RejectedExecutionException("Task rejected: " + r);
}
};
三、线程池使用中的常见问题
-
资源耗尽
- 问题 :线程池过大(如
maximumPoolSize
设置过高)可能导致 CPU 或内存耗尽。 - 解决:根据任务类型(CPU 密集型 vs. I/O 密集型)合理配置线程数。
- 问题 :线程池过大(如
-
死锁与任务依赖
- 问题 :线程池中的任务相互等待(如使用
Future.get()
阻塞线程)。 - 解决 :避免任务间循环依赖,或使用
CompletableFuture
异步编排。
- 问题 :线程池中的任务相互等待(如使用
-
任务堆积导致 OOM
- 问题 :使用无界队列(如
LinkedBlockingQueue
)时,任务激增可能导致内存溢出。 - 解决:改用有界队列,并配合合理的拒绝策略。
- 问题 :使用无界队列(如
-
线程泄漏
- 问题 :线程未正确关闭(如未调用
shutdown()
),或任务抛出未捕获异常导致线程终止。 - 解决 :使用
try-catch
包裹任务逻辑,并通过afterExecute()
处理异常。
- 问题 :线程未正确关闭(如未调用
-
上下文切换开销
- 问题:线程数过多时,频繁的线程切换会降低性能。
- 解决:通过监控工具(如 Arthas)分析线程状态,优化线程池参数。
-
异常处理缺失
- 问题 :使用
execute()
提交任务时,未捕获异常会导致线程终止且无日志。 - 解决 :使用
submit()
提交任务并通过Future.get()
捕获异常,或在任务内部处理异常。
- 问题 :使用
四、最佳实践
-
合理配置参数
- CPU 密集型任务:
corePoolSize = CPU 核心数
。 - I/O 密集型任务:
corePoolSize = CPU 核心数 * 2
。
- CPU 密集型任务:
-
监控线程池状态
- 通过
ThreadPoolExecutor
的getActiveCount()
、getQueue().size()
等方法实时监控。
- 通过
-
避免全局线程池
- 不同业务使用独立线程池,防止相互影响。