Java 线程池
在Java并发编程中,线程是稀缺且昂贵的系统资源。直接创建和销毁线程会带来三个核心问题:
- 资源消耗大:线程的创建/销毁涉及操作系统内核态与用户态的切换,频繁操作会占用大量CPU时间片,导致系统吞吐量下降。
- 稳定性风险:无限制创建线程会耗尽内存资源(每个线程默认栈大小1MB),最终触发
OutOfMemoryError。 - 管理成本高:无法统一控制线程的生命周期、任务队列长度及并发数上限,易导致系统失控。
线程池的核心价值在于线程复用与统一管理:它像一个"线程工厂",提前创建并维护一批线程,任务提交时直接分配线程执行,避免重复创建销毁的开销,同时通过参数约束系统资源占用。
# 一、ThreadPoolExecutor:线程池的核心实现
ThreadPoolExecutor是Java线程池的核心实现类,其行为由七大核心参数共同决定。理解这些参数是掌握线程池的基础。
# 1.1 核心参数详解
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 非核心线程空闲存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 工作队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
)
2
3
4
5
6
7
8
9
# (1)核心线程数(corePoolSize)
- 线程池的"常驻线程"数量,即使空闲也不会被销毁(除非通过
allowCoreThreadTimeOut(true)开启核心线程超时销毁)。 - 任务提交时的处理逻辑:
- 若当前线程数 < corePoolSize:直接创建新线程(核心线程)执行任务。
- 若当前线程数 ≥ corePoolSize:任务进入工作队列等待。
# (2)最大线程数(maximumPoolSize)
- 线程池允许创建的最大线程总数(核心线程 + 非核心线程)。
- 约束逻辑:当核心线程满、工作队列也满时,线程池会创建非核心线程执行任务,直到线程数达到
maximumPoolSize。 - 注意:
corePoolSize ≤ maximumPoolSize。若两者相等,则线程池为"固定大小"(无临时线程)。
# (3)空闲存活时间(keepAliveTime + unit)
- 非核心线程空闲超过该时间后,会被销毁以释放资源。
- 特殊配置:通过
allowCoreThreadTimeOut(true)可让核心线程也遵守该规则(适用于资源紧张场景,如临时任务高峰后释放资源)。
# (4)工作队列(workQueue)
- 核心线程满后,新任务会先进入队列等待,而非直接创建非核心线程。
- 常用队列类型及适用场景:
| 队列类型 | 特点 | 适用场景 | 风险提示 |
|---|---|---|---|
| ArrayBlockingQueue | 有界队列,需指定初始容量 | 生产环境首选,可控制任务堆积上限 | 容量需合理设置(过小易触发拒绝) |
| LinkedBlockingQueue | 无界队列(默认容量Integer.MAX_VALUE) | 不推荐!任务堆积易导致OOM | 高并发下风险极高 |
| SynchronousQueue | 同步队列,不存储任务(直接传递) | 任务需快速处理,不允许排队的场景 | 依赖最大线程数控制并发 |
| PriorityBlockingQueue | 优先级队列,按任务优先级排序 | 任务有明确优先级区分的场景(如紧急任务) | 需自定义任务的compareTo方法 |
# (5)线程工厂(ThreadFactory)
- 用于创建线程,可自定义线程名称、优先级、是否为守护线程等属性。
- 默认工厂:
Executors.defaultThreadFactory(),创建的线程为非守护线程,名称格式为pool-{poolNum}-thread-{threadNum}。 - 自定义示例(便于问题排查):
ThreadFactory customThreadFactory = new ThreadFactory() {
private final AtomicInteger threadNum = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, "order-pool-thread-" + threadNum.getAndIncrement());
thread.setDaemon(false); // 非守护线程(避免主线程退出后被强制终止)
thread.setPriority(Thread.NORM_PRIORITY); // 正常优先级
return thread;
}
};
2
3
4
5
6
7
8
9
10
# (6)拒绝策略(RejectedExecutionHandler)
- 当线程池达到
maximumPoolSize且工作队列已满时,新提交的任务会触发拒绝策略(线程池的最后一道防线)。
# 1.2 线程池核心工作流程
任务处理遵循核心线程 → 工作队列 → 非核心线程 → 拒绝策略的顺序,步骤如下:
- 提交任务时,若当前线程数 < corePoolSize:创建核心线程执行任务。
- 若核心线程满(≥ corePoolSize):任务进入工作队列等待。
- 若队列满且当前线程数 < maximumPoolSize:创建非核心线程执行任务。
- 若队列满且线程数 ≥ maximumPoolSize:执行拒绝策略。
示例:设corePoolSize=2,maximumPoolSize=4,workQueue=ArrayBlockingQueue(2)
- 任务1、2:创建核心线程执行。
- 任务3、4:进入队列等待。
- 任务5、6:创建非核心线程执行(总线程数达4)。
- 任务7及以后:触发拒绝策略。
# 二、拒绝策略:线程池的"安全阀"
ThreadPoolExecutor内置4种拒绝策略,同时支持自定义策略,需根据业务场景选择:
# 2.1 内置策略
- AbortPolicy(默认)
- 直接抛出
RejectedExecutionException,中断任务提交流程。 - 适用场景:核心业务任务(需明确感知任务提交失败,避免静默丢失)。
- 直接抛出
new ThreadPoolExecutor.AbortPolicy()
- CallerRunsPolicy
- 由提交任务的线程(调用者线程)直接执行任务,会阻塞调用者。
- 适用场景:并发量不大的场景或需要限流(通过阻塞调用者降低任务提交速度)。
new ThreadPoolExecutor.CallerRunsPolicy()
- DiscardPolicy
- 直接丢弃新任务,不抛异常(任务无声丢失)。
- 适用场景:非核心任务(如日志采集、统计上报,丢失不影响核心流程)。
new ThreadPoolExecutor.DiscardPolicy()
- DiscardOldestPolicy
- 丢弃队列中最旧的任务,尝试重新提交当前任务。
- 适用场景:任务有先后顺序,且旧任务可丢弃(如实时数据处理,旧数据时效性低)。
new ThreadPoolExecutor.DiscardOldestPolicy()
# 2.2 自定义策略
实现RejectedExecutionHandler接口,可添加日志记录、任务持久化(如存入数据库/消息队列)等逻辑:
RejectedExecutionHandler customHandler = (runnable, executor) -> {
// 记录被拒绝的任务信息
log.error("任务被拒绝,当前线程池状态:活跃线程数={}, 队列大小={}, 任务详情={}",
executor.getActiveCount(),
executor.getQueue().size(),
runnable.toString());
// 可选:将任务存入Redis,后续重试
redisTemplate.opsForList().leftPush("rejected_tasks", JSON.toJSONString(runnable));
};
2
3
4
5
6
7
8
9
# 三、线程池的状态:生命周期管理
ThreadPoolExecutor定义了5种状态,控制线程池的生命周期及任务处理行为:
| 状态 | 含义 | 触发方式 |
|---|---|---|
| RUNNING | 正常运行:接收新任务,处理队列中已有任务 | 线程池初始化后默认状态 |
| SHUTDOWN | 不接收新任务,但继续处理队列中已有任务 | 调用shutdown() |
| STOP | 不接收新任务,中断正在执行的任务,清空队列 | 调用shutdownNow() |
| TIDYING | 所有任务执行完毕,线程数为0,即将进入终止状态 | 线程池从SHUTDOWN/STOP过渡到此状态 |
| TERMINATED | 线程池彻底终止,执行完terminated()钩子方法(可自定义资源清理逻辑) | 从TIDYING状态过渡,标志生命周期结束 |
状态转换流程:
RUNNING → SHUTDOWN → TIDYING → TERMINATED
或
RUNNING → STOP → TIDYING → TERMINATED
# 四、为什么不推荐使用Executors工具类?
Executors提供了线程池工厂方法(如newFixedThreadPool、newCachedThreadPool),但为了简化使用牺牲了可控性,存在潜在风险:
# 4.1 无界队列导致OOM(newFixedThreadPool / newSingleThreadExecutor)
- 两者均使用
LinkedBlockingQueue(默认容量Integer.MAX_VALUE,约20亿)。 - 风险:当任务提交速度远大于处理速度时,任务会无限制堆积在队列中,耗尽堆内存触发OOM。
# 4.2 无限制创建线程导致资源耗尽(newCachedThreadPool)
- 核心线程数=0,最大线程数=Integer.MAX_VALUE,空闲超时60秒。
- 风险:高并发下会无限制创建线程,导致CPU上下文切换频繁、内存耗尽,甚至触发操作系统"进程内存限制"。
结论:生产环境应直接使用ThreadPoolExecutor,通过显式参数控制资源上限。
# 五、参数配置最佳实践
合理配置线程池参数是发挥其性能的关键,需结合任务类型(CPU密集/IO密集)和系统资源评估:
# 5.1 核心线程数与最大线程数
CPU密集型任务(如计算、排序、加密):
线程数 = CPU核心数 + 1(+1是为了避免CPU空闲,当某线程因页缺失等阻塞时,其他线程可利用CPU)。
示例:4核CPU → 5个线程。IO密集型任务(如数据库查询、网络请求、文件IO):
线程数 = CPU核心数 / (1 - 阻塞系数)(阻塞系数通常为0.8~0.9,即任务80%~90%时间在等待IO)。
示例:4核CPU → 4 / (1 - 0.8) = 20个线程。
# 5.2 工作队列选择
- 必须使用有界队列(如
ArrayBlockingQueue),避免无界队列导致的OOM。 - 队列容量需根据业务压测确定:过小易触发拒绝策略,过大则占用内存过多。建议初始值设为核心线程数的5~10倍,再通过监控调优。
# 5.3 其他建议
- 线程工厂:自定义线程名称(如
order-process-pool-thread-1),便于日志排查和监控。 - 空闲时间:非核心线程空闲时间建议设为30~60秒(平衡资源释放与任务响应速度)。
- 拒绝策略:核心任务用
AbortPolicy(快速失败),非核心任务用自定义策略(日志+持久化)。
# 六、总结
线程池是Java并发编程的核心工具,其核心价值在于通过线程复用减少资源消耗,通过参数约束保证系统稳定。使用时需注意:
- 直接使用
ThreadPoolExecutor,避免Executors工具类的隐藏风险。 - 根据任务类型(CPU/IO密集)合理配置核心参数,尤其是核心线程数、最大线程数和有界队列。
- 选择合适的拒绝策略,并通过监控及时发现线程池异常。