☰
Java线程池线上排查与调优:核心参数、避坑要点与监控实践
2026/10/13 8:03:59 网站建设 项目流程

接到一个线上排查任务,现象是某接口响应时间从80ms慢慢爬到3秒多,随后大量请求开始报超时。翻监控一看,线程池的活跃线程数拉满,任务队列堆了十几万条,后续提交的任务陆续被拒。线程池这种基础组件,平时不出问题岁月静好,一出问题基本都是连锁反应。

这篇文章我不打算按教科书节奏从零科普,而是把自己处理过的线程池相关问题系统梳理一遍:核心参数怎么定、任务提交之后池内发生了什么、哪些坑必须先避开、故障怎么定位、以及事后怎样靠监控数据反向调优。正在入门并发编程的同学可以当复习提纲,已经在线上被线程池折腾过的同行,看看有没有和你踩过一样的坑。

1. 线程池核心设计:为什么非它不可

1.1 手动建线程的代价与池化逻辑

先说最基础的问题:为什么非要用线程池,直接new Thread不好吗?

在没有池化的情况下,每处理一个请求就创建一个线程,等请求处理完再销毁。单个线程的创建和销毁看起来很快,但在高并发场景下,这些开销会被放大得非常恐怖。创建线程至少要分配独立的线程栈空间,Java里默认栈大小通常是1MB,系统层面还要创建对应的内核线程、做调度初始化;销毁线程又要释放这些资源。假设你的接口每秒进来500个请求,每个请求一个线程,一秒内线程的创建销毁次数就是500次,CPU大量时间花在系统调用上,业务还没跑起来,机器的负载先上去了。

操作系统对线程数量也并非没有上限,当线程数超过机器可承受的阈值,系统会频繁切换上下文甚至直接报内存溢出错误,进程失去响应。线程池做的事情,本质上和数据库连接池、对象池是一样的:资源复用加总量控制。把线程这个稀缺资源提前备好,反复使用,同时加一道阀门限制并发总量。理解了池化逻辑,后面所有参数都围绕“复用什么、控制多少”来理解,会顺畅很多。

1.2 七个核心参数拆解

ThreadPoolExecutor最完整的构造函数一共有七个参数,业务方配置线程池时看到的也是这一组。逐个拆开看,每个参数都有明确的职责范围。

  • corePoolSize:核心线程数,也就是线程池正常情况下常驻的线程数量。默认情况下,核心线程即使一直空闲,也不会被回收。注意一个容易忽略的细节:核心线程并不是在创建线程池时就全部就位,而是随着任务提交逐渐创建的。

  • maximumPoolSize:最大线程数,线程池能容纳的线程上限。当任务太多、队列放不下的时候,线程池会在核心线程之外临时创建额外线程,最多到这个值。

  • keepAliveTime和unit是一对,表示非核心线程的空闲存活时间。临时创建的多余线程,如果空闲时间超过这个阈值,会被回收,避免高峰期过后资源白白占用。

  • workQueue:任务队列。核心线程都忙不过来时,新任务会先进入队列等待。队列的类型和容量,直接决定了线程池在面临流量冲击时的行为,这一块我单独拿出来讲。

  • threadFactory:线程工厂,用来定义线程的创建方式,最常见的是给线程起一个有意义的名字。别小看这个名字,线上排查线程池问题,有时候靠的就是线程名里的关键信息。

  • handler:拒绝策略。当线程数已经达到maximumPoolSize且队列也满了,再提交的任务会交给handler处理。

七个参数之间不是孤立的。core决定常态承载能力,max决定峰值承载能力,队列负责削峰缓冲,keepAliveTime控制资源回收速度,handler兜底防止无限堆积。一个合理配置的线程池,是在这些约束之间找到适合业务节奏的平衡点,而不是把所有参数全部拉满。

我用餐厅后厨来打比方:日常客流4个厨师就够了,高峰期客人涌进来,前厅排队,队排得太长时,总店临时增派人手最多到10人;高峰期过去,临时帮工离开,只留4个常驻。如果客流大到后厨10个人都处理不过来,就只能挂出暂停接客的牌子。线程池的设计思路,和这几乎一模一样。

1.3 工作队列选型差异

队列特性典型影响
ArrayBlockingQueue有界、基于数组队列满后会触发扩容或拒绝,可控性强
LinkedBlockingQueue默认容量Integer.MAX_VALUE,可设置容量无界时任务无限堆积
SynchronousQueue不存任务,直接交接适合任务多、希望立即创建线程执行的场景
PriorityBlockingQueue无界、支持优先级任务按优先级执行,但同样存在堆积风险

队列选型之所以重要,是因为它决定了线程池的缓冲水位。ArrayBlockingQueue容量定多少,意味着最多积压多少任务,超过之后立刻触发扩容或拒绝,整体行为可控。LinkedBlockingQueue默认容量是Integer.MAX_VALUE,也就是无界,任务永远能塞进队列,这时maximumPoolSize形同虚设,因为队列从未满过,线程池根本没有机会扩容。很多线上故障,根源就是用了无界队列,任务无限堆积,延迟越来越高,最终内存和响应时间双双失控。

SynchronousQueue有点特别,它本身不暂存任何任务,提交一个任务就要被某个线程立即取走,没有空闲线程时offer会失败,从而促使线程池新建线程。CachedThreadPool用的就是这种组合。

PriorityBlockingQueue支持任务按优先级出队,适合某些调度型场景,但因为它也是无界队列,同样要注意堆积问题,并且队列元素必须实现Comparable接口,不然运行时会直接报错。

提示:日常业务代码里,我最常用的是ArrayBlockingQueue,原因是“有界”能保证线程池在异常流量下仍然有一个明确的承载边界,后续的拒绝策略、监控告警才有意义。无界队列表面看更宽容,实际是把问题从线程池转移到了内存和延迟上,后期更难收拾。

2. 任务提交之后:线程池内部流转与线程复用

2.1 四步流转规则与认知误区

很多人背过线程池的任务提交流程,但实际踩坑时发现顺序和自己想的不太一样。ThreadPoolExecutor处理一个新提交任务的完整顺序是:

  1. 如果当前线程数小于corePoolSize,直接创建新线程执行这个任务。这里有个容易忽略的细节,判断只看线程数量是否小于corePoolSize,不管当前有没有线程空闲。也就是说,即使某个核心线程正闲着,只要总线程数没到core,新任务来了还是会再创建一个新线程。

  2. 如果当前线程数已经达到或超过corePoolSize,尝试把任务放进workQueue。入队成功,任务先排队,后续由空闲线程取走执行。

  3. 如果队列已经满了,入队失败,再判断当前线程数是否小于maximumPoolSize。如果小于,创建新线程来执行这个任务,这个线程属于临时扩容线程。

  4. 如果线程数已经达到maximumPoolSize,队列又满了,线程池无路可走,直接执行拒绝策略。

这里最大的认知误区,是很多人以为线程池的扩容顺序是“先创建线程,线程满了再排队”。真实流程恰恰相反:先排队,队列满了才扩容。这样设计是有道理的——线程数量的增加会带来上下文切换和内存占用,能不用就不用;而队列作为缓冲,可以暂时消化一部分短时流量波动,让线程数尽量保持稳定。

注意:反过来的误区也常见,有人把corePoolSize设得很大,以为能提前备好线程。线程池创建时不会预创建线程,而是懒加载机制,任务来了才逐渐创建到core。如果你希望高峰期不因为临时建线程产生毛刺,可以调用prestartAllCoreThreads()提前启动所有核心线程。

2.2 线程复用的关键机制

线程池之所以叫池,关键在于线程复用。线程池内部每个工作线程被包装成了一个Worker对象,Worker持有Thread和一个初始任务,Thread的run方法内部是一个大循环,不断从队列里取任务执行,取到就run,取不到就阻塞等待。任务完成后线程不会退出,而是继续等待下一个任务。这样一来,一条物理线程能服务成千上万个任务,创建线程的成本被均摊到每次执行上,复用率非常高。

这个机制下还有一个相关参数需要知道:默认情况下核心线程空闲了也不会退,除非你调用allowCoreThreadTimeOut(true),这样核心线程空闲超过keepAliveTime也会被回收,线程池的线程数可能降到0。这个特性适合业务有明显低峰期、想省资源的场景,但要慎重,因为每一次从0到有线程都会重新承担创建开销。

线程复用之外,线程池的生命周期状态也值得熟悉。从RUNNING到SHUTDOWN、STOP、TIDYING再到TERMINATED,每一步状态变化都会影响任务的接纳和执行。日常最需要关注的是:调用了shutdown()之后,线程池不再收新任务,但队列里的老任务还会继续执行完;shutdownNow()则会尝试中断正在跑的任务,并返回还没执行的任务列表。这个区别在第三章谈优雅关闭时会具体展开。

2.3 拒绝策略:四种方案怎么选

策略行为适用场景与风险
AbortPolicy(默认)直接抛出RejectedExecutionException显式暴露问题,配合告警;调用方需处理异常
CallerRunsPolicy让提交任务的线程自己执行任务降低吞吐,起到天然限流作用;可能拖慢调用方
DiscardPolicy静默丢弃简单但任务凭空消失,不推荐
DiscardOldestPolicy丢弃队列中最老的任务保障新任务,但老任务可能更重要,需评估

绝大多数线上项目,我建议保留默认的AbortPolicy,或者包装一层统计后仍然抛异常。理由很简单:任务被拒是重要事件,你需要让它显性化。一旦出现拒绝,立刻收到告警日志和指标增长,才能推动你去扩容或改造;如果静默丢弃,业务方根本不知道任务没了,订单可能就无声无息地丢了。

CallerRunsPolicy在少数场景下是好用的——线程池饱和时,用提交请求的线程去执行任务,相当于把压力反馈回调用方,系统不会因为继续堆积而崩溃。但如果提交方是Web容器线程,被阻塞的任务会占用容器线程,接口RT飙升,最终可能拖垮整个接入层,这个代价要能接受才用。

经验:真正成熟的项目通常不是换一个拒绝策略就完事,而是把拒绝策略当作报警开关。我会自定义handler,先记录拒绝次数和堆栈摘要,再抛RejectedExecutionException,这样排查时能直接定位到是哪个业务线程池在满负荷,而不是面对一个干巴巴的异常对象。

3. 业务场景中的线程池配置实操

3.1 线程数估算:从公式到压测

线程池参数到底怎么配,是很多人最头疼的地方。没有万能公式,但有两个经典口径可以先用起来,判断任务类型再算初值。

如果是CPU密集型任务,线程数通常设为CPU核数+1。因为这类任务几乎不阻塞,线程多了只会增加上下文切换,核数+1是为了在个别线程发生偶发IO或GC停顿时有替补线程顶上,保持CPU利用率。

如果是IO密集型任务,线程数经验公式是CPU核数乘以(1 + IO等待时间 / CPU计算时间)。比如机器8核,计算耗时1ms,等待IO耗时9ms,那么线程数可以估算为8 * (1 + 9/1) = 80。IO等待时间越长,同时间能并行等待的线程就越多,线程数就可以开得更高。

混合型任务就要先拆分。如果任务里既有计算又有IO,一个线程池很难用一个线程数表达清楚。我遇到过一类通知服务,任务里先算签名、再等数据库、再调外部渠道,整条链路的耗时分布完全不同。这种情况先把任务拆成“计算阶段、IO阶段”两个池,或者给IO阶段单独设池,否则调参只能靠猜。

所有公式都只是起点,最终参数一定来自压测和线上监控校准。先按公式给一个初值,然后在准生产环境模拟高峰流量,观察活跃线程数、队列水位、RT,再回头调整,这一步是必须的。

3.2 一份可以直接抄的配置模板

ThreadPoolExecutor orderExecutor = new ThreadPoolExecutor( 8, // corePoolSize 16, // maximumPoolSize 60L, TimeUnit.SECONDS, // 非核心线程空闲60秒回收 new ArrayBlockingQueue<>(1000), // 有界队列,最多积压1000个任务 new NamedThreadFactory("order-process"), // 自定义线程工厂 new ThreadPoolExecutor.AbortPolicy() // 显式抛出拒绝异常 );

配套的线程工厂示例:

public class NamedThreadFactory implements ThreadFactory { private final String prefix; private final AtomicInteger sequence = new AtomicInteger(1); public NamedThreadFactory(String prefix) { this.prefix = prefix; } @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r, prefix + "-" + sequence.getAndIncrement()); return thread; } }

这段配置的核心是:有界队列 + 显式拒绝策略 + 命名线程工厂。有界队列保证任务积压有上限,不会无限撑爆内存;AbortPolicy让任务被拒时立刻抛异常,方便监控;命名线程工厂保证出问题时你能在jstack里一眼认出来。

队列容量1000怎么来的?这里针对一个任务处理服务,正常并发下每秒任务量大概两三百,单个任务平均耗时50ms,队列容量保守给到核心线程一分钟消化量的两到三倍左右,不至于频繁触发扩容,也不会积压太久。实际数字当然还是要压测,但思路是这样的:队列容量要能扛住短时流量尖峰,又不能大到让问题隐形。

如果想在拒绝时统计次数,可以包装默认策略:

private final AtomicLong rejectedCounter = new AtomicLong(); ThreadPoolExecutor.AbortPolicy basePolicy = new ThreadPoolExecutor.AbortPolicy(); RejectedExecutionHandler countingHandler = (r, e) -> { rejectedCounter.incrementAndGet(); basePolicy.rejectedExecution(r, e); };

实操心得:项目里最好固定一套线程池管理类,统一封装创建、指标上报、关闭逻辑。业务方不允许自己new裸线程池,必须通过管理类来拿。这样可以保证线程命名、队列策略、拒绝策略都是统一的,排查问题的效率会高很多。

3.3 优雅关闭的正确姿势

很多服务上线之后很少重启,但一旦碰到发布、缩容、停机,线程池怎么关闭就成了一个问题。直接kill进程会导致正在执行的任务被突然中断,状态可能停留在中间阶段;但盲目调用shutdownNow()也不好,它会中断正在运行的线程,如果任务对中断敏感,会造成数据不一致。

推荐的关闭顺序是先调用shutdown(),再等待一段时间,最后才shutdownNow():

orderExecutor.shutdown(); if (!orderExecutor.awaitTermination(30, TimeUnit.SECONDS)) { orderExecutor.shutdownNow(); if (!orderExecutor.awaitTermination(60, TimeUnit.SECONDS)) { // 仍然没结束,记录日志并人工介入 } }

这里的关键是理解shutdown()和shutdownNow()的区别。shutdown()只是把线程池状态切成SHUTDOWN,不再接收新任务,但对队列里已经提交的任务还是会一一执行完;awaitTermination()是等线程池真正结束;如果等了30秒还没结束,说明任务执行时间太长或存在阻塞,此时再用shutdownNow()强制中断。

还有一点,如果任务执行过程里需要通知下游系统,关闭线程池前最好先停止接收新任务,预留一个缓冲期,让队列里的老任务自然排空,避免业务状态到一半系统就没了。

4. 线上线程池故障排查与避坑实录

4.1 Executors工厂方法的隐患

很多项目图省事,直接用Executors静态方法创建线程池。这几个方法看着方便,埋的雷却不小。

newFixedThreadPool和newSingleThreadExecutor底层用的是LinkedBlockingQueue,默认容量是Integer.MAX_VALUE。前面说过,无界队列意味着maximumPoolSize完全失效,任务永远在排队。高峰流量来的那一刻,系统会把大量任务压进队列,内存持续上涨,接口RT被拉长,最后要么内存溢出,要么请求在超时边缘反复横跳。

newCachedThreadPool则走向另一个极端,最大线程数是Integer.MAX_VALUE,队列是SynchronousQueue。只要没有空闲线程,提交任务就会无脑新建线程,线程数可以涨到让系统资源彻底耗尽的水平。平时流量低时它表现很好,处理完任务线程空闲60秒被回收,但一旦遇到突发流量,线程数蹭蹭往上涨,光线程栈就能吃掉大量内存。

注意:不要在生产环境直接使用Executors的这几个静态方法。倒不是说它一无是处,而是它的默认参数满足不了生产环境的边界控制需求。你需要的不是“无限容量”,而是“有明确上限、有兜底策略、可观测”的池。

4.2 任务悄悄丢失的三类场景

第一类是提交方式造成的。用execute()提交任务时,任务内部抛出的异常最终会传到线程的UncaughtExceptionHandler,如果没设置,可能只看到一段残缺堆栈;用submit()提交时更隐蔽,任务内部的异常会被封装进Future,如果你拿Future之后根本没调get(),异常会被吞得干干净净。我见过不少线上案例,任务明明提交了,却没有任何结果落地,日志里也找不到异常,最后才发现异常被Future吞掉了。更麻烦的是,Worker线程因为异常退出后,线程池会补一个新线程,日志里看起来像是线程数在抖动,单看堆栈很难定位是哪条任务出的问题。

第二类是拒绝策略造成的。DiscardPolicy和DiscardOldestPolicy都会静默丢弃任务,业务侧完全感知不到。如果系统依赖任务的最终执行,这种丢弃就是生产事故。

第三类是执行线程内部的异常被错误捕获。有些代码为了“防止任务崩溃”,在Runnable里catch了Throwable,再打印日志,但日志级别设成了DEBUG,生产环境根本看不到。异常信息就这么“消失”了。使用线程池时,建议利用线程池提供的afterExecute钩子,统一记录任务异常,而不是在任务内部各自吞掉。

4.3 线程池嵌套、死锁与ThreadLocal脏数据

线程池还有一个容易踩的坑是嵌套依赖。比如线程池A里的任务要调用B线程池来执行另一个任务,并且同步等待B的结果,如果两个池的线程都被占满、各自都在等对方完成任务,就会形成线程池级别的死锁。这种问题从jstack里能看到两个线程池的线程都在WAITING或PARKING状态,互相等Future结果,外层任务则源源不断触发超时。

另一个高频踩坑点是ThreadLocal在线程池中的脏数据。普通单线程模型下,用完ThreadLocal,线程销毁数据自然消失;但线程池里的线程是复用的,如果任务里往ThreadLocal塞了用户信息却没有remove,下一个任务拿到同一个线程时,get出来的还是上一个任务的数据。最典型的表现是用户串号、traceId串了。解决办法是在任务开始显式set,在finally里显式remove,不要依赖线程销毁来清理。

提示:ThreadLocal用在线程池里,核心原则就是“谁set谁remove,必须在finally里remove”。不要寄希望于线程销毁,因为线程根本不会销毁。

4.4 排查工具、命令与速查表

线程池问题第一步排查动作,是抓现场。建议先看两个东西:线程池指标和线程堆栈。指标看活跃线程数和队列水位,堆栈看线程名和状态。如果你的线程工厂已经命名好,jstack一次就能定位:

jstack <pid> | grep -A 20 "order-process"

-A 20的意思是显示匹配行之后20行,这样能把该线程的堆栈完整带出来,能看到它到底是阻塞在业务SQL上、锁等待上,还是只是从队列里取不到任务的正常等待。

如果现场已经过去,就看历史监控。活跃线程数、队列大小、拒绝次数三件套,是判断线程池问题的核心水位线。被动排查和主动观测两手都要抓。

症状常见原因排查方向建议处理
接口RT持续升高队列无限堆积queueSize持续上涨有界队列、控制积压、增加处理能力
任务静默消失submit不get/Future未消费检查Future使用、拒绝策略统一异常上报、改用execute或调用get
线程数量暴涨CachedThreadPool看线程名与线程数不用无界线程池,设置上限
用户数据串号ThreadLocal未清理检查任务是否removefinally里remove
进程退出迟迟不结束未优雅关闭检查shutdown调用shutdown+awaitTermination
下游线程池饱和拖垮上游嵌套依赖查看线程池调用链路独立隔离、降级限流

5. 用监控指标反向驱动线程池调优

5.1 关键指标与获取方法

线程池调优不能靠拍脑袋,数据才是依据。ThreadPoolExecutor对外提供了几个便于监控的方法:

  • getPoolSize():当前线程池的线程数量。
  • getActiveCount():正在执行任务的线程数。
  • getQueue().size():队列中积压的任务数。
  • getCompletedTaskCount():已完成任务数。
  • getTaskCount():历史提交的总任务数,近似值。
  • getLargestPoolSize():线程池曾经达到的最大线程数,用来评估峰值需求。

这些方法组合起来能算出几个关键水位:活跃线程占比、队列占用率、拒绝次数增量、任务执行速率。有了这些,你才能判断当前配置是偏紧还是偏松。

5.2 一个简易监控采样器

实际项目我会用一个定时任务,每30秒把线程池的状态打一条日志。不是每次提交任务都打印日志,那样日志量太大,定时采样足够看到趋势了。参考实现如下:

int queueCapacity = 1000; ScheduledExecutorService sampler = Executors.newSingleThreadScheduledExecutor(r -> { Thread t = new Thread(r, "tp-monitor"); t.setDaemon(true); return t; }); sampler.scheduleAtFixedRate(() -> { int poolSize = orderExecutor.getPoolSize(); int active = orderExecutor.getActiveCount(); int queueSize = orderExecutor.getQueue().size(); long completed = orderExecutor.getCompletedTaskCount(); long rejected = rejectedCounter.get(); double activeRatio = poolSize == 0 ? 0 : (double) active / poolSize; double queueRatio = queueCapacity == 0 ? 0 : (double) queueSize / queueCapacity; String line = String.format("[tp-monitor] pool=%d active=%d activeRatio=%.2f queue=%d/%d queueRatio=%.2f completed=%d rejected=%d", poolSize, active, activeRatio, queueSize, queueCapacity, queueRatio, completed, rejected); logger.info(line); }, 0, 30, TimeUnit.SECONDS);

日志字段解读是监控的核心。activeRatio高说明线程都在干活,线程数可能不够;activeRatio低但queueRatio高,说明任务在排队,但不是线程不够,而是要分析任务是否卡在IO或锁上;rejected开始增长,说明线程池已经到硬上限了,这个信号最强。

实操心得:这条监控日志我通常先以info级别打印,上线观察一周后稳定了再降成debug。排查周期问题靠它,值班的人也能从日志里直观判断线程池状态,比闷头盯监控面板快得多。

5.3 动态调整参数的几个细节

线程池参数不一定要改代码才能调整,运行时通过setCorePoolSize和setMaximumPoolSize可以动态扩缩。但这里有几个容易误解的细节。

调大maximumPoolSize之后,并不会立刻创建新线程,而是等到后续提交任务时发现队列满,并且当前线程数小于新max,才会真正创建。所以如果你想尽快扩容,需要配合提高提交速度,或者做一次预热。

调大corePoolSize同样不是即时生效。如果当前线程数小于新core,要等新任务提交时逐步创建到core;想让所有核心线程立即可用,调用prestartAllCoreThreads()。如果当前线程数大于新core,空闲线程会随着keepAliveTime走完被回收。

调小参数时也要注意,正在执行任务的线程不会被中断,它们要跑完当前任务才会退出。所以动态缩容是一个渐进过程,不会瞬间击穿正在跑的任务。

orderExecutor.setCorePoolSize(16); orderExecutor.setMaximumPoolSize(32); orderExecutor.prestartAllCoreThreads(); // 需要立即拉起核心线程时调用

触发策略我建议用指标驱动:持续N个采样周期内queueRatio高于阈值,就扩容;持续M个周期低于阈值,就收缩。手动自动都行,关键是阈值要基于历史监控数据标定,不要凭感觉设一个数。

5.4 指标异常时怎么调

最后给几个常见的指标异常场景,直接对应调整思路。

如果queueRatio长期偏高,且activeRatio很高,说明任务处理能力不足,优先考虑调用方能否把任务拆小、减少阻塞,或者适当提升线程池的core和max。

如果activeRatio长期很低,但queueRatio依旧高,问题通常不在线程数量上,而是任务在某个环节卡住了,比如等待外部接口、竞争锁、依赖下游线程池。这时加线程只能加剧资源竞争,应该去查任务阻塞点,而不是无脑调参。

如果rejectedCount出现增长,说明现在资源已经顶到天花板。此时最优先的动作是限流降级,保护核心链路,其次才是扩容。若把线程数继续加大,整体RT可能会继续恶化。

如果completedTaskCount长期不增长,而activeCount又大于0,很可能是线程卡死或任务死循环,需要抓jstack确认,而不是去改参数。线程池不是万能的,参数调优解决的是容量错配问题,解决不了死锁和长阻塞。

聊到最后,说一点这几年和线程池打交道最深的感觉。线程池本身只是一个组件,90%的问题都不在线程池的源码里,而在它的使用姿势——用了无界队列、线程没有命名、任务里嵌套另一个线程池、ThreadLocal没清理、参数全靠拍脑袋、没有拒绝策略监控。如果项目里把线程命名、有界队列、拒绝策略告警、指标监控这四件事都做好,线程池出问题的概率会低非常多。

最后再分享一个小技巧:给线程池提交任务时,可以先把这个池最重要的几个指标打一条info日志,比如当前队列水位、活跃线程数,稳定运行后再降级。别看它简单,遇到线上RT抖动的时候,这条日志能直接帮你判断到底是任务进来了执行不完,还是任务根本没进来。线程池这东西,提前花半小时做观测,后面能少熬好几个通宵。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询