线程池
约 2336 字大约 8 分钟
2025-08-25
使用线程池的好处
线程池通过资源管理和线程复用,避免了频繁创建和销毁线程带来的性能开销。线程创建和销毁涉及系统调用、内存分配和上下文切换,资源消耗较高。线程池预先创建并维护一组线程(核心线程),在任务提交时直接复用空闲线程,而非新建线程。这降低了系统资源消耗(如 CPU 和内存),提高了任务处理效率和系统吞吐量。同时,线程池提供了任务队列、拒绝策略等机制,增强了系统的稳定性和可控性。
线程池的核心参数
线程池的核心行为由以下七个参数定义(以 ThreadPoolExecutor 为例):
- 核心线程数 (
corePoolSize):线程池长期维持的最小线程数量,即使空闲也不会被销毁。 - 最大线程数 (
maximumPoolSize):线程池允许创建的最大线程总数(包括核心线程和临时线程)。 - 临时线程存活时间 (
keepAliveTime):当线程数超过核心线程数时,临时线程在空闲状态下的最大存活时间。 - 存活时间单位 (
unit):临时线程存活时间的单位(例如TimeUnit.SECONDS)。 - 任务阻塞队列 (
workQueue):用于缓存待执行任务的队列(例如ArrayBlockingQueue或LinkedBlockingQueue)。 - 线程工厂 (
threadFactory):用于创建新线程的工厂类(例如ThreadFactory接口实现),可定制线程名称、优先级等属性。 - 拒绝策略 (
rejectedExecutionHandler):当任务队列满且线程数达到最大值时,处理新提交任务的策略(例如AbortPolicy)。
线程池的工作流程
线程池处理任务的流程遵循以下逻辑:
- 任务提交时,优先分配至空闲的核心线程执行。
- 若核心线程均处于工作状态(满负载),任务进入阻塞队列等待。
- 若阻塞队列已满,线程池创建临时线程(非核心线程)执行新任务,但总线程数不超过最大线程数。
- 当线程数达到最大线程数且队列满时,触发拒绝策略处理新任务。
任务拒绝策略及其选择
当线程池无法接受新任务时,拒绝策略定义处理行为。常见策略包括:
AbortPolicy:默认策略,抛出RejectedExecutionException异常。适用于允许部分任务失败的非关键场景(如日志记录),但需谨慎使用以避免关键任务丢失(如支付订单)。DiscardPolicy:静默丢弃新任务,不抛出异常。适用于允许少量任务丢失的低优先级场景(如临时缓存更新或心跳上报)。DiscardOldestPolicy:移除队列中最早的任务,并尝试提交新任务。适用于实时性要求高的场景(如流数据处理),其中新任务比旧任务价值更高。CallerRunsPolicy:将任务回退至提交任务的调用线程执行。适用于不允许任务丢失且可接受延迟的场景(如批处理作业),能自然降低提交速率。
选择原则:根据业务重要性、任务实时性和系统容错能力决策。关键任务优先选用 CallerRunsPolicy 或定制策略;非关键任务可选 DiscardPolicy 或 AbortPolicy。
核心线程数和最大线程数的设定
线程数的设定需结合任务类型和系统资源,推荐分步优化:
CPU 密集型任务(如复杂计算):
\text{线程数} \approx \text{CPU 核数} + 1
原因:任务主要消耗 CPU,线程上下文切换开销高,过多线程会降低性能。
I/O 密集型任务(如网络或磁盘操作):
\text{线程数} \approx 2 \times \text
原因:I/O 等待时 CPU 可切换线程。
线程数可参考公式:
线程数=CPU 核数×(1+线程等待时间/线程运行总时间)
- CPU 密集型中线程等待时间无线趋近于 0。
- IO 密集型中线程等待时间无限趋近于线程总运行时间。
在实际需求中,应先根据公式确定一个初始值,再结合实际业务情况来实践优化:
- 压测:模拟高并发场景(如 JMeter 工具),调整线程数以最大化 CPU 利用率(目标 70%-80%)和最小化任务平均耗时。
- 上线后监控:结合运行时指标(如队列堆积情况)动态调整。
注
线程数过多会导致频繁上下文切换,降低性能;需考虑应用中的其他线程和混合任务类型(如 I/O 与计算并存),建议核心线程数固定,最大线程数预留缓冲。
项目上线后线程池的监控指标
为保障线程池稳定运行,需监控以下指标:
运行时状态指标
- 核心线程数:当前活跃的核心线程数量。 若低于配置值,可能线程回收过度;若等于配置值且任务堆积,需增大核心线程数。
- 活跃线程数:正在执行任务的线程总数。 若持续接近最大线程数,表明负载过高,需扩容或优化任务逻辑。
- 队列大小:任务队列中的待处理任务数。 若长期满载,表明任务处理慢,需增大队列容量或提升任务效率。
任务处理效率指标
- 任务提交速率:单位时间提交的任务数。 结合任务完成速率评估吞吐量饱和度。
- 任务完成速率:单位时间完成的任务数。 若低于提交速率,存在堆积风险。
- 任务平均耗时:任务从提交到完成的平均时间。 耗时突增可能因性能问题(如慢 SQL)。
- 任务等待时间:任务在队列的平均等待时间。 过长等待表明核心线程不足或任务执行慢。
资源占用指标
- CPU 使用率:线程池所在节点的 CPU 占用。 高 CPU 使用率但低吞吐量可能因锁竞争;低 CPU 使用率但队列堆积可能因 I/O 阻塞。
- 内存占用:线程池任务消耗的内存(堆或直接内存)。 关注是否有大对象或内存泄漏风险(如 OOM)。
异常情况指标
- 拒绝任务数:被拒绝的任务总数。 若拒绝任务数过高,则调整线程数或队列容量。
- 线程创建/销毁频率:单位时间线程的生命周期变化。 频繁变化(线程池震荡)可能因存活时间过短或任务具有突发性。
- 线程阻塞时间:线程因 I/O 或锁等待的阻塞时间占比(可通过
ThreadMXBean或 APM 工具如 SkyWalking 监控)。 - 死锁检测:监控线程死锁(使用
ThreadMXBean.findDeadlockedThreads())。
为什么不推荐使用 JDK 的 Executors 创建线程池
Executors 是 JDK 提供的线程池工厂类,但其返回的线程池存在资源耗尽风险,不推荐使用:
newFixedThreadPool和newSingleThreadPool- 问题:这两个方法创建的线程池使用的是
LinkedBlockingQueue作为任务队列,而该队列的默认容量是Integer.MAX_VALUE(约 21 亿)。这意味着任务队列几乎是“无界”的,当任务提交速度远超线程池处理速度时,队列会不断积累任务,最终可能耗尽内存,导致内存溢出。 - 场景:例如,在高并发场景下,如果任务执行时间较长,提交的任务会持续堆积,队列占用的内存会无限制增长,最终导致系统崩溃。
- 问题:这两个方法创建的线程池使用的是
newCachedThreadPool- 问题:
newCachedThreadPool创建的线程池允许根据任务需求动态创建线程,最大线程数也是Integer.MAX_VALUE。在高并发场景下,如果任务提交速度很快,线程池可能会创建大量线程(每个线程都会占用系统资源,如内存和 CPU)。这可能导致:- 内存溢出:大量线程的创建会消耗大量内存(每个线程的栈空间通常在 512KB 到 1MB 之间)。
- 系统崩溃:过多线程竞争 CPU 资源,可能导致上下文切换开销激增,系统性能严重下降,甚至崩溃。
- 场景:例如,短时间内接收到大量任务请求,线程池会无限制地创建新线程,最终耗尽系统资源。
- 问题:
推荐替代方案:直接使用 ThreadPoolExecutor 构造函数,显式指定参数(如队列容量和线程数上限),规避资源风险并提供更细粒度控制。
动态线程池
在实际应用中,任务负载动态变化且环境复杂(如混合 I/O 与计算),需实现动态线程池以灵活调优:
- 核心思路:将线程池参数(如核心线程数、最大线程数)从代码迁移至分布式配置中心(如 Nacos 或 Apollo),支持运行时动态调整并即时生效。
- 实施步骤:
- 监控线上指标(如队列堆积和 CPU 使用率)。
- 通过配置中心更新参数,无需重启服务。
- 结合压测结果持续优化,确保线程池适应业务峰值。