1. 线程池简介
线程池(Thread Pool)是一种基于池化思想管理线程的工具;
优势
降低资源消耗:重复利用已经创建的线程,降低频繁创建/销毁线程的开销
提高响应速度:任务到达时,无需创建线程可立即执行
提高线程的可管理性:线程是稀缺资源,无限制创建会影响系统资源失衡,降低系统稳定性,使用线程池可以统一进行线程分配,调优,监控。
更强大功能:如延时定时线程池
2、线程池的使用
1:利用ThreadPoolExecutor提供的构造方法创建线程池
publicThreadPoolExecutor(intcorePoolSize,intmaximumPoolSize,longkeepAliveTime,TimeUnitunit,BlockingQueue<Runnable>workQueue,ThreadFactorythreadFactory,RejectedExecutionHandlerhandler){......}线程池的核心参数
核心线程数corePoolSize:
- 默认情况下线程初始化时不会创建线程,(除非调用了 prestartCoreThread() 或 prestartAllCoreThreads())。
- 当任务来临时才会创建核心线程
- 核心线程即使在空闲情况下也会一直存活
最大线程数maximumPoolSize:
- 线程池允许创建的最大线程数,核心线程数+非核心线程数
- 只有当前线程数大于或等于核心线程数并且任务队列已满时才会创建非核心线程(且不超过最大线程数)
- 非核心线程超过空闲时间会被回收
- 最大线程数已满,执行拒绝策略
存活时间keepAliveTime
- 非核心线程空闲超过此存活时间会被自动回收
- 不存在非核心线程时参数无效。如果调用了 allowCoreThreadTimeOut(true),核心线程也会被回收。
时间单位unit:keepAliveTime的时间单位
任务队列workQueue:当前线程数已达到核心线程数,就会将新进来的线程任务放入队列中
- 用于保存线程任务的工作队列
- 当池子里的工作线程数 >= corePoolSize,新来的线程任务就会进入队列。
- 如果队列已满 ,且当前线程数 < maximumPoolSize 创建创建非核心线程
- 当池子里的工作线程数已达 maximumPoolSize,拒绝该线程任务。
线程工厂threadFactory:创建线程的接口,线程池通过它创建线程,
- 默认使用Executors.defaultThreadFactory()
- 可以实现ThreadFactory接口,自定义线程工厂
拒绝策略handler:当线程池无法接收新任务时调用拒绝策略
- 线程池已满(任务队列已满 + maximumPoolSize已满)
- 线程池关闭:shutdown() 或 shutdownNow() 之后,不能再提交新任务
任务队列:
ArrayBlockingQueue:基于数组实现,创建时必须指定容量。队列长度有限,当队列满了就会创建非核心线程执行任务。
LinkedBlockingQueue基于单向链表的有界/无界队列(无界时容量为 Integer.MAX_VALUE),当任务处理速度跟不上任务创建速度会导致内存占用过多,甚至OOM。
SynchronousQueue队列长度为 0,它不存储元素,每个 put 必须等待一个 take,反之亦然
拒绝策略:
AbortPolicy:默认拒绝策略,中断抛出RejectedExecutionException异常
CallerRunsPolicy:让提交任务的主线程来执行任务,最重要每个业务全部执行完
DiscardOldestPolicy:丢弃在队列中存在时间最久的任务,重复执行。根据系统是否允许丢弃老业务进行衡量
DiscardPolicy:丢弃任务,不抛出异常,可能无法发现系统的异常状态,建议无关紧要的业务使用此策略
另外可以实现RejectedExecutionHandler接口,自定义拒绝策略
2、Executors提供的静态方法创建线程池
newFixedThreadPool(int nThreads)创建一个定长的线程池
特点:
- 核心线程数 = 最大线程数,默认不会回收线程
- 队列为 无界 的 LinkedBlockingQueue,可以无限积压任务。
场景
- 任务数量稳定且可预期,处理速度与提交速度基本匹配
- 可接收任务在队列长时间等待
问题
- OOM : 提交速度高于处理速度时,队列无线增长,最终耗尽内存
- 故障难以恢复:一旦队列积压严重,即使后续系统恢复,也需要很长时间才能消化完积压任务。
newSingleThreadExecutor()创建一个单线程的线程池
特点:
- 核心线程数 = 最大线程数 = 1,单线程顺序执行所有任务(类似 newFixedThreadPool(1)),不需要处理线程同步问题。
- 队列为无界 LinkedBlockingQueue
- 如果线程因异常终止,会创建一个新线程代替。
场景:
- 不适合并发场景
- 需要保证任务按提交顺序串行执行(FIFO)。
- 轻量级的异步处理,如写日志、发通知等。
问题
- OOM :同样存在队列无限增长,最终耗尽内存问题
- 单线程处理能力有限,后续任务会无限排队。
newCachedThreadPool()可缓存线程池
特点:
- 核心线程数 = 0,最大线程数 = Integer.MAX_VALUE,keepAliveTime = 60 秒,
- 没有核心线程,任务提交后立刻尝试创建新线程,队列 SynchronousQueue 容量为 0。
- 只要当前线程数 < Integer.MAX_VALUE,就会创建新线程
- 空闲线程存活 60 秒后自动回收。
场景:
- 任务并发量波动大,但每个任务都很轻量。
- 执行数量多、耗时少的任务
问题:
- 线程数无上限:高并发时可能创建数百万线程,导致系统资源耗尽。
- 如果任务执行时间较长下,新任务会不断创建新线程,系统很快崩溃。
newScheduledThreadPool(int corePoolSize) 定时线程池
特点:
- 核心线程数 = 指定值,maximumPoolSize = Integer.MAX_VALUE,keepAliveTime = 0
- 无界队列 DelayedWorkQueue ,永远不会满,所以 maximumPoolSize 参数在这里基本失效。
- 线程数始终等于 corePoolSize(最多也就这些)
场景:
- 并发执行多个定时/延时任务
- 定时任务执行时间较长,单线程会阻塞其他任务,需要用多线程来提升吞吐量。
问题 - OOM : 无界队列
- 线程数有限:最多使用 corePoolSize 个线程去执行它们,其余任务在队列中等待。
newSingleThreadScheduledExecutor() 单线程定时线程池
特点:
- corePoolSize = 1,maximumPoolSize = 1,keepAliveTime = 0(无效,因为没有非核心线程)
- DelayedWorkQueue无界队列
- 所有定时/延时任务按提交顺序,在该单线程上串行执行
- 如果线程因异常终止会自动创建一个新线程替代
场景:
- 后台单线程执行周期任务(如:每 5 秒清理一次缓存)
- 多个延迟任务之间有严格的顺序要求(但注意如果任务耗时不同,可能会乱序)。
- 不想让定时任务互相干扰(单线程天然避免并发)。
问题:
- OOM
- 任务阻塞会拖垮整个调度器:只有一个线程执行
- 任务异常未捕获:如果任务抛出未捕获的异常,默认不会打印堆栈(只会在 Future.get() 时抛出),容易导致静默失败。建议任务内部 try-catch 或设置 UncaughtExceptionHandler。
线程池的提交任务
1:无返回值提交
voidexecute(Runnablecommand);适用:不需要返回结果的任务(比如记录日志、异步清理、发送通知)。
行为:任务执行后没有返回值,也无法得知任务是否成功完成(除非在 Runnable 内部自己处理异常)。
异常:如果任务抛出未捕获的异常,线程池会记录并打印(但不会影响其他任务),该线程会被回收并创建新线程(对于普通 ThreadPoolExecutor)。
风险:你无法获知任务执行完毕的时机,也无法得到异常信息。
2:有返回值的提交
Future<?>submit(Runnabletask);//提交Runnable任务返回值:Future<?>,调用 future.get() 会返回 null,但可以用于阻塞等待任务完成或捕获异常。
<T>Future<T>submit(Runnabletask,Tresult);//提交Runnable任务并指定执行结果返回值:Future,future.get() 返回一个预先定义的结果(常用于某些框架中)。
<T>Future<T>submit(Callable<T>task);//提交Callable任务回值:Future,future.get()阻塞等待并返回 Callable 计算的结果,或抛出 ExecutionException(包装实际异常)。
3:批量提交:invokeAll 与 invokeAny
invokeAll(Collection<?extendsCallable<T>>tasks)提交所有任务,并阻塞直到所有任务都完成(正常完成或抛出异常)。
返回 List<Future>,顺序与输入集合一致。
可以通过每个 Future.isDone() 检查,或 get() 获取结果(会立即返回,因为已经完成)
也支持超时版本:invokeAll(Collection, timeout, unit),返回指定时间内完成的执行结果,取消未完成的任务。
invokeAny(Collection<?extendsCallable<T>>tasks)提交所有任务,只要有一个任务成功完成(不抛异常),就返回该任务的结果,其他任务被取消。
阻塞直到有第一个成功结果或所有任务都失败(抛出 ExecutionException)。
也支持超时版本:invokeAny(Collection, timeout, unit)返回指定时间内最先完成的执行结果,取消未完成的任务
Future 的常用方法
**get() :**阻塞等待任务完成并获取结果(或抛异常)
get(timeout, unit):阻塞等待,超时则抛 TimeoutException
**isDone():**判断任务是否已完成(正常、异常、取消)
**isCancelled():**判断任务是否被取消
**cancel(boolean mayInterruptIfRunning) :**尝试取消任务。如果任务尚未开始或正在运行(且参数为 true),则中断执行线程。返回 true 表示取消成功。
** 4:执行周期、重复性任务**
ScheduledExecutorService 专门用于延迟执行或周期性执行任务。通常通过 Executors 的工厂方法创建:
newSingleThreadScheduledExecutor() – 单线程,任务串行执行
newScheduledThreadPool(int corePoolSize) – 多线程,可并发执行多个周期任务
两种周期性执行方法:
publicScheduledFuture<?>scheduleAtFixedRate(Runnablecommand,longinitialDelay,longperiod,TimeUnitunit)以固定频率运行,从上一个任务开始计时。如果任务执行时间超过周期,下一个任务会立即开始(不会并发,而是排队等待该线程空闲)
publicScheduledFuture<?>scheduleWithFixedDelay(Runnablecommand,longinitialDelay,longdelay,TimeUnitunit)以固定延迟运行,上一个任务结束后,等待固定时长再执行下一个。任务执行时间不影响间隔基准
关闭线程池
1:优雅关闭shutdown()
- 线程池不再接收新任务。已提交的任务(包括正在执行和队列中等待的)会继续执行完毕。后续进来的新任务会被执行拒绝策略
- 所有任务完成后,线程池真正终止(isTerminated() 返回 true)。
- 注意:调用后线程池不会立即退出,需要配合 awaitTermination 等待完成。
- 线程池状态 SHUTDOWN
2:强制关闭shutdownNow
- 立即关闭线程池,正在执行中的任务和队列中的任务都会被中断(通过 Thread.interrupt(),但任务需要自行响应中断)。
- 返回被中断的队列中的任务列表,包含队列中尚未开始的任务(这些任务不会被执行了)。
- 线程池会尽力终止,但不保证一定能停掉所有任务(例如任务忽略中断)。
- 线程池状态 STOP
isTerminated() :所有任务都已执行完毕:包括正在执行的任务和队列中等待的任务。(是非阻塞的状态检查)
awaitTermination(long timeout, TimeUnit unit) :在超时时间内,线程池是否已完全终止,是返回true,否返回false(阻塞等待状态检查)
线程池执行任务的具体流程
提交任务时,查看 线程数 是否 小于 核心线程数,如果小于创建新线程,并执行当前任务。如果大于则查看任务队列是否满了,如果未满加入队列,等待执行。如果队列已满,查看线程数是否小于最大线程数,如果小于创建新线程执行此任务,如果不小于执行拒绝策略;
注:
- 提交任务时,不管当前线程池线程是否空闲,只要线程数小于核心线程数,都会创建新线程执行任务。
- 线程池执行任务是非公平的,队列满了之后提交的任务会比队列中排队的·任务先执行。
线程池状态流转
线程池的五种状态
1、RUNNING:正常运行状态,可以接收新任务并处理队列中任务
2、SHUTDOWN:正在关闭,不会接收新任务,但会处理队列中已有的任务
3、STOP:立即关闭,不接收新任务,不处理队列中已有任务,并且尝试中断正在执行任务(注意:一个任务能不能被中断得看任务本身)
4、tidying:过渡状态,当所有任务已经终止,线程池无线程,状态就会转为TIDYING,并调用线程池terminated()。
5、TERMINATED:终止状态,terminated()方法执行完毕。
状态转换关系
RUNNING │ ├── 调用 shutdown()──────────→ SHUTDOWN │ │ │ └── 队列为空且线程池中没有线程 → TIDYING → TERMINATED │ └── 调用 shutdownNow()────────→ STOP │ └── 线程池中没有线程,所有任务终止 → TIDYING → TERMINATED手动调用 shutdown() 从 running状态转为 shutdown状态,不在接收任务,继续执行已有任务。当队列为空所有任务执行完后shutdown状态转为tidying状态,执行terminated()方法后状态为terminated。
手动调用 shutdownNow()从 running状态转为 stop状态,不在接收新任务,并尝试中断正在执行任务。所有任务(包括正在执行的和队列中的)都已终止(被中断或完成)转为tidying状态,执行terminated()方法后状态为terminated。
1、RUNNING -> SHUTDOWN:手动调用shutdown()触发,或者线程池对象GC时会调用finalize()从而调用shutdown()
2、RUNNING -> STOP:调用shutdownNow()触发
3、SHUTDOWN ->STOP:先调shutdown()紧着调shutdownNow()
4、SHUTDOWN -> TIDYING:队列为空并且线程池中没有线程时自动转换
5、STOP -> TIDYING:线程池中没有线程时自动转换(队列中可能还有任务)
6、TIDYING -> TERMINATED:terminated()执行完后就会自动转换
判断当前状态
**isShutdown():**当状态为 SHUTDOWN、STOP、TIDYING、TERMINATED 时返回 true(即不是 RUNNING)。
**isTerminated():**当状态为 TERMINATED 时返回 true。
**isTerminating():**当状态为 SHUTDOWN 或 STOP 时返回 true(表示正在关闭过程中)。
线程池中的线程是如何关闭的
shutdown():空闲线程中断退出;忙碌线程完成任务后自然退出。
shutdownNow(): 尝试中断所有线程,任务队列被清空并返回;线程响应中断后退出。
阻塞队列的作用:
如果队列中没有任务时,线程为了不立刻消亡,就会堵塞在获取任务时,当队列中有任务就会拿到新任务执行。
非核心线程使用超时堵塞获取,如果时间内没有获取到新任务就消亡了。
线程发生异常,会被移出线程池吗?
会的,但线程池会额外再新增一个线程,这样就能维持住固定的核心线程数。
线程池的参数设计分析
1、设置初始参数
方式一:根据预估单线程处理能力设置
1、核心线程数
每个任务的处理时间,每秒的处理任务数量,每秒产生的任务数量
例如:
每个任务的处理时间0.1秒,则每秒可以执行10 (1s / 0.1)个任务,系统80%的时间每秒产生100个任务。
如果在一秒中完成任务,则需要10个线程。理论情况下核心线程数可以设置10。
实际情况会存在流量波动和调度开销,一般采用二八原则设计,即80%情况设计核心线程数,20%利用最大线程数处理。
2、任务队列长度
设计阻塞队列可以缓存多少个任务。一般设计为:核心线程数 / 单个任务执行时间 *2 即可。
如上述场景:核心线程数10 / 单个任务执行时间0.1 * 2 = 200
3、最大线程数
设计时参照核心线程数与每秒产生的最大任务数;
如:每秒产生的最大任务数 1000。 则最大线程数= ( 最大任务数 - 任务队列长度) * 单个任务执行时间
即:(1000 - 200) *0.1 = 80个
4、最大空闲时间
建议 30~300 秒。如果流量波动大且需要快速回收,可设短一些(60s)
这个参数的设计完全参考系统运行环境和硬件压力设定,没有固定的参考值,用户可以根据经验和系统产生任务的时间间隔合理设置一个值即可。
方式二:根据任务类型设置参数
IO密集型:指任务执行时间主要花费在等待I/O操作上,非CPU计算,此时线程大部分时间处于堵塞状态,CPU使用率低。
使用多线程提高cpu使用率,从而提高整体吞吐量。
当一个线程执行IO操作时,CPU会将其设置为阻塞状态,此时CPU可以去执行其他就绪线程。增加线程数可以在一个线程等待IO时,其他线程继续使用CPU,提高整体吞吐量。
场景:数据库查询、文件读写、网络通信、远程缓存操作等
先从这个区间开始压测:
corePoolSize=CPU核数*2maximumPoolSize=CPU核数*2~3queue=有界队列CPU密集型:任务执行时间几乎全部花费在CPU计算上,会持续占用CPU,对CPU使用率要求极高。
场景:图像/视频处理、加密/解密、数据压缩、大数据排序等
先从这个区间开始压测:
corePoolSize=CPU核数 maximumPoolSize=CPU核数+1// +1 是为了防止因页缺失或偶尔线程阻塞导致 CPU 空闲。queue=中小型有界队列2、通过压测工具(JMeter)和监控的具体情况分析,修改参数继续压测,找到最适合的参数
常见情况:
1:活跃线程数经常打满
线程池的 activeCount 长时间等于 maximumPoolSize,说明所有工作线程都在忙碌。
可能出现:
- 任务执行时间太长(或者堵塞),
- 线程池容量偏小,无法处理当前负载,
- 下游系统过慢,导致线程堵塞积压
解决方式:
1:增加线程,但要确定系统cpu,内存,下游资源有余量。
2:优化任务执行效率,减少耗时,增加缓存,减少循环,优化sql 等;
3:jstack分析线程是否有堵塞,死锁等问题
4:拆分任务,大任务拆成多个小任务,避免长时间占用线程。
2:队列长度持续增长
queue.size() 不断上升,甚至接近或超过队列容量。
可能原因:
- 任务生产速度 > 任务处理速度,线程能力处理不足
- 核心线程过少,队列设置过大,抑制线程扩容
- 任务执行时间长或者堵塞
解决方式:
1:提升消费速度,增大线程数,优化任务逻辑,拆分任务
2:抑制生产速度,让提交任务的线程也执行任务,自然降低提交任务的频率
3:启用动态线程池(如 DynamicTp),在线调整队列容量或线程数。
3:CPU 负载高
CPU 使用率持续 ≥ 85%~90%,系统负载高。
可能原因:
- cpu密集型任务,负载达上限
- 线程数过多导致频繁上下文切换,cpu大量花费在调度上。
- 代码问题
解决方式:
1、判断任务类型:cpu密集型,减少线程数约为cpu核数,避免切换开销。io密集型说明本身计算量很大,需要优化代码
2、降低任务提交速率,保护系统。
4:数据库连接池吃紧
连接池的 活跃 连接数经常打满 最大线程数,获取连接超时或等待
可能原因:
- 线程池连接数 > 数据库连接数,大量线程竞争连接
- 任务中连接过长,如存在大事务,连接未关闭等
- 数据库响应慢,连接被长时间占用
解决方式:
1、减少线程池连接数,确保最大线程数不要超过数据库连接上限,避免线程空等。
2、数据库连接优化,如使用连接时尽量短事务,及时释放。使用批量操作减少连接请求次数。
3、引用本地缓存,减少对数据库的访问
4、数据库读写分离,分散压力
5、第三方接口开始超时
现象:调用外部服务时出现 TimeoutException 或响应缓慢。
可能原因:
- 下游系统处理能力不足。
- 网络延迟高或带宽不足。
- 线程池线程数过多,对下游造成过载(每线程一个请求,打满下游连接池或导致限流)。
- 未设置合理的超时时间,导致线程长时间阻塞。
解决方式:
1、限流保护:在调用下游的代码中使用熔断器(Hystrix、Resilience4j)或限流器。
2、控制线程池大小,避免对下游造成冲击。
3、调用时增加超时重试(需幂等)。使用异步调用(如 CompletableFuture)避免阻塞。
4、增加缓存:对于读多写少的场景,缓存下游返回结果。
线程池调优的目标,不是把参数配得最大,而是找到吞吐、延迟、稳定性之间的平衡点。
3、使用DynamicTp 参数动态化
线程池参数一旦在代码上定义,就固定下来,修改参数必须要重新修改代码编译打包部署重启。而线上流量存在随机性与不确定性,无论经过多少次压测都很难预估出完美参数,而参数偏差及容易引起系统故障。线程数参数设置过小当流量高峰时线程池无法处理新任务,导致任务失败。线程数设置过大又会导致过多线程调度,和内存占用,降低系统吞吐量。队列过大,导致大量任务积压在队列。且开发人员无法感知到线上系统线程池内部运行状态。
使用动态线程池将参数从代码剥离,由配置中心统一管理,通过配置中心动态刷新机制,在不重启服务前提下,根据系统流量实时修改线程池配置,快速恢复优化系统,并且提供实时监控能力,如线程池活跃线程数,队列长度,拒绝任务数等,基于这些数据还能配置告警,问题发生时及时通知开发人员。
配置线程池时遵循原则
1:确定任务类型:IO/CPU密集型
2:使用有界队列,防止 OOM 与任务堆积。接口类场景优先用有界队列,线上接口最怕的是任务无限堆积,最终把延迟拖崩。
3:线程数必须与下游资源容量匹配。
4:为拒绝策略设置明确的降级逻辑,拒绝策略是系统超载时的最后一道防线,必须可控、可观测。
5:动态调整优先于静态固化,不要在代码中硬编码线程池参数。将参数配置化,支持运行时调整
6:参数配置宁可通过压测逐步放大,也不要一上来配得特别激进。
7:必须有监控
- 活跃线程数(activeCount)
- 队列长度(queue.size())
- 拒绝次数(累计或速率)
- 任务平均耗时 / TP99
- 线程池任务完成率
- 结合系统指标:CPU 使用率、内存、下游连接池状态。
- 设置告警:队列积压超过阈值、拒绝次数 > 0、活跃线程数持续等于 maximum-
- 没有监控的线程池就像一个黑盒,故障发生后才被动响应,代价极高。