新闻详情

新闻详情

首页 / 资讯中心 / 详情

TIMED_WAITING 线程堆积排查:Java线程池参数与快照实战

发布时间:2026/10/1 1:18:44来源:尧图网络
TIMED_WAITING 线程堆积排查:Java线程池参数与快照实战
半夜被电话叫起来告警内容是应用线程数突破两千持续增长。登上机器 jstack 一把捞出来满屏都是java.lang.Thread.State: TIMED_WAITING第一反应是线程池堵了有死锁结果顺着栈一个个看下去才发现真正堵住的只有十几个剩下的一千九百多个全是空闲等待、定时等待甚至有些本来就是这个线程池该有的正常形态。TIMED_WAITING这个状态在 Java 线程池排查里几乎是最容易被误读的一个。它既可能是超出核心线程数的空闲线程在等自己过期这种完全健康的现象也可能是线程无限增殖、池被反复创建、下游把线程全部拖住的前兆。如果你只是看到数量多就下结论很容易把方向带偏改了一圈参数问题还在甚至把本来正常的收缩机制给关掉了。这篇内容我会从线程状态的语义开始一路讲到线程快照的归类方法、我在生产里遇到的四类真实堆积场景、可照抄的排查脚本以及队列选型、线程数设定、存活时间这几个旋钮到底该怎么拧。适合正在被线程数告警折磨的后端同学也适合想把线程池配置从头理一遍的工程师。1. 把 TIMED_WAITING 的语义先捋清楚1.1 六种状态里它是最容易被误判的一个Java 线程状态一共六种NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。这里面真正需要警惕的是BLOCKED等锁和长时间不动的RUNNABLE可能死循环、可能卡在 native。WAITING和TIMED_WAITING都属于主动让出 CPU、在等一个信号的状态区别只在于——WAITING是无限期等TIMED_WAITING是带超时的等。带超时这件事本身不构成问题。问题在于数量和增长趋势。一个处于TIMED_WAITING的线程它已经把自己的执行权交出去了操作系统调度器基本不会去管它CPU 占用接近零。所以大量 TIMED_WAITING几乎不会是 CPU 飙高的原因。它真正的代价在内存和内核资源上线程栈默认 1MB64 位 Linux 下-Xss默认值两千个线程就是约 2GB 的虚拟地址空间再加上线程本地变量、内核里每个线程一份的 task_struct以及可能触发的unable to create new native thread。提示看到线程数告警时先分清是CPU 被谁吃掉了和内存被谁吃掉了两个独立问题。TIMED_WAITING 属于后者。1.2 getTask() 里那一行 poll才是线程池 TIMED_WAITING 的最大来源看懂线程池的线程为什么是TIMED_WAITING只要看ThreadPoolExecutor.getTask()这一个方法就够。它的核心逻辑简化后是这样private Runnable getTask() { boolean timedOut false; for (;;) { int c ctl.get(); int wc workerCountOf(c); // 关键判定是否需要限时等待 boolean timed allowCoreThreadTimeOut || wc corePoolSize; try { Runnable r timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) // 限时等等不到就退出 : workQueue.take(); // 无限等 if (r ! null) return r; timedOut true; } catch (InterruptedException retry) { timedOut false; } } }timed这个布尔值是分水岭。它成立的三种情况allowCoreThreadTimeOut true也就是你显式允许核心线程也超时销毁当前工作线程数wc大于corePoolSize也就是存在富余线程。只要timed为真走的就是poll(keepAliveTime, NANOSECONDS)底层落到LockSupport.parkNanos()JVM 层面就显示成TIMED_WAITING (parking)。反之走take()底层是LockSupport.park()显示成WAITING (parking)。所以当你把corePoolSize设为 4、maximumPoolSize设为 200 的时候任何一个瞬间只要线程数超过了 4多出来的那些线程在空闲时必然是 TIMED_WAITING。这不是 bug这是线程池的弹性收缩机制在工作。1.3 同样是 TIMED_WAITINGparkNanos 和 sleep 完全是两回事栈顶方法决定了这个线程到底在等什么。常见的几类栈顶特征典型触发代码性质Unsafe.parkparkNanosLinkedBlockingQueue.pollgetTask超出 core 的空闲线程等过期正常关注数量Unsafe.parkparkNanosSynchronousQueue$TransferStack.awaitFulfillcached 池空闲线程等任务正常但 max 无界时危险Unsafe.parkparkNanosConditionObject.awaitNanosDelayedWorkQueue.take定时任务池等下一个触发点正常Thread.sleep业务自己写的轮询、客户端清理线程看类名归属Object.wait(timeout)老式同步客户端、连接池补偿线程看类名归属Future.get(timeout)业务代码在等下游返回偏异常说明被拖住了Thread.join(timeout)框架关闭流程、测试代码少见前三种是框架级的等待是设计如此后几种才是需要往业务和第三方组件里找线索的。这个表后面会反复用到。2. 用线程快照把正常等待和异常堆积拆开2.1 拿快照的三种姿势各有适用场景第一种是jstack pid dump.txt最常规。注意必须用和 Java 进程同一个用户执行否则会拿不到锁信息。加-l会额外打印 ownable synchronizers排查死锁时很有用但排查线程数量问题时只会让输出体积翻倍建议不加。第二种是jcmd pid Thread.print。当进程线程数超过三千、jstack卡住或者超时的时候jcmd通常更稳因为它走的是 attach 机制对目标进程的暂停时间更短。第三种是kill -3 pid。这个会把线程栈直接打到进程的标准输出里——Tomcat 场景下就是catalina.out。它的好处是不依赖任何外部工具、不受用户权限限制坏处是位置分散、需要自己捞。线上环境工具链不齐全的时候这招是救命用的。注意如果jstack返回Unable to open socket file或者直接挂住不动不要反复重试目标进程可能处于长时间的 safepoint 等待中先看 GC 日志确认有没有 Full GC 风暴。2.2 先数总量再按栈顶归类拿到快照后的第一步永远是数数而不是读栈。数总量的命令# 线程总数 grep -c ^ dump.txt # 各状态数量 grep -o java.lang.Thread.State: [A-Z_]* dump.txt | sort | uniq -c | sort -rn第二步按栈顶方法归类。因为线程栈的格式是状态行 缩进的 at 行用grep -A 1抓第一帧最省事grep -A 1 State: TIMED_WAITING dump.txt \ | grep -E ^\sat \ | awk {print $2} \ | sed s/(.*// \ | sort | uniq -c | sort -rn | head -30这条命令的输出会直接告诉你在所有 TIMED_WAITING 线程里有多少卡在LinkedBlockingQueue.poll有多少卡在SynchronousQueue有多少卡在Thread.sleep。这一步做完方向基本就定了七成。2.3 两次快照间隔 30 秒看的是趋势不是绝对值单次快照只能看到此刻有多少两次快照才能看到它在涨还是在稳。间隔 30 秒比较合适太短看不到变化太长又会拖慢排查节奏。jstack $PID d1.txt sleep 30 jstack $PID d2.txt echo d1 threads: $(grep -c ^ d1.txt) echo d2 threads: $(grep -c ^ d2.txt) # 对比线程名前缀分布有没有新增 grep ^ d1.txt | awk -F {print $2} | sed s/-[0-9]*$// | sort | uniq -c | sort -rn n1.txt grep ^ d2.txt | awk -F {print $2} | sed s/-[0-9]*$// | sort | uniq -c | sort -rn n2.txt diff n1.txt n2.txt数量稳定、只是基数大多半是配置问题数量持续爬升、且新增的都是同一类线程名那就是泄漏。这两种情况的修复方向完全不同前者调参后者找代码。3. 我在生产里真正遇到过的四类堆积3.1 cached 池加无界 max一波突发流量之后线程集体滞留这是最经典的一种。代码大概长这样ExecutorService pool Executors.newCachedThreadPool();newCachedThreadPool的内部参数是corePoolSize0、maximumPoolSizeInteger.MAX_VALUE、keepAliveTime60s、队列是SynchronousQueue。含义是来一个任务如果有空闲线程就复用没有就立刻新建一个最多能建到二十多亿个空闲 60 秒后回收。平时 QPS 低池里就两三个线程。一旦有个批量任务进来比如一次性提交八千个请求池子会瞬间膨胀到几千个线程每个线程建栈就要耗时上下文切换成本也会上来。任务处理完的那一瞬间这几千个线程全变成TIMED_WAITING整齐地卡在SynchronousQueue$TransferStack.awaitFulfill上等 60 秒后慢慢退场。那 60 秒就是告警窗口期。如果监控的采样周期正好落在这个窗口里你会看到一条笔直向上的线程数曲线然后慢慢回落。这种情况下不是 bug但池子设计确实有问题——maximumPoolSize给到无界等于放弃了自我保护。修复方式很直接换成手写的ThreadPoolExecutormaximumPoolSize定一个基于容量测算的硬上限keepAliveTime缩到 10~30 秒队列用SynchronousQueue配CallerRunsPolicy或者自定义的降级策略。3.2 池被反复创建线程只生不灭这一种最阴。表现是线程数单调上涨、从不回落而且线程名前缀高度重复。典型写法public void handle(Request req) { ExecutorService pool Executors.newFixedThreadPool(8); pool.submit(() - doSomething(req)); }每次请求建一个池用完不shutdown。JVM 里的线程对象只有在线程run()方法返回后才会终止而ThreadPoolExecutor的工作线程会一直循环在getTask()里等任务所以池对象即使变成垃圾线程也依然活着。时间一长线程数就是请求数乘以 8。这种问题在线程快照里的特征是线程名都是pool-N-thread-M而 N 是一个很大且在持续增长的数字。Executors默认的线程工厂用的是全局AtomicInteger poolNumber所以这个 N 会一直加下去。看到pool-3000-thread-1这种名字基本可以直接断定有人在反复建池。修复分三步一是把池提升为类级别或容器管理的单例二是给Bean加上destroyMethod或者在 Spring 里用ThreadPoolTaskExecutor让容器负责shutdown三是全局搜索Executors.new逐个人工确认生命周期。心得Executors.newFixedThreadPool还有一个隐患是队列用的是无界LinkedBlockingQueue任务堆积时不会拒绝、只会撑内存。阿里规约禁用Executors的快捷方法主要就是冲着这两点来的。3.3 定时任务池的 DelayedWorkQueue 长期 awaitNanosScheduledThreadPoolExecutor的corePoolSize默认是 1maximumPoolSize是Integer.MAX_VALUE但它的工作队列是DelayedWorkQueuetake()方法被重写了public RunnableScheduledFuture? take() throws InterruptedException { final ReentrantLock lock this.lock; lock.lockInterruptibly(); try { for (;;) { RunnableScheduledFuture? first queue[0]; if (first null) available.await(); // 队列空无限等 - WAITING else { long delay first.getDelay(NANOSECONDS); if (delay 0) return finishPoll(first); available.awaitNanos(delay); // 还没到点限时等 - TIMED_WAITING } } } finally { lock.unlock(); } }所以如果你有大量每 5 分钟执行一次每天凌晨 2 点执行的定时任务每个任务一个ScheduledExecutorService那么在两次触发之间每个池的工作线程都会显示TIMED_WAITING栈顶是ConditionObject.awaitNanos中间能清楚地看到DelayedWorkQueue.take。这种属于完全正常。它唯一值得优化的地方是如果每个定时任务都单独一个池那么线程数等于定时任务数。几十个定时任务就是几十个线程谈不上危险但如果上百个就该合并到一个共享的调度池里了。3.4 第三方客户端的清理线程被误当成业务线程池还有一种情况线程多的肇事者根本不在你的代码里。常见的几个来源连接池的保活/清理线程定期扫描空闲连接消息客户端的重连线程、心跳线程缓存客户端的定时刷新线程日志异步 Appender 的AsyncAppender-Worker线程。这些线程通常只有一两个不会造成线程数爆炸但它们的名字往往很像业务线程池比如都叫pool-1-thread-1、Thread-12很容易在做线程名归类的时候被混进去让人误判成业务池泄漏。区分方法有两个。一是看栈里有没有明显的框架包名比如com.zaxxer.hikari.pool.HikariPool、com.alibaba.druid.pool、org.apache.logging.log4j.core.appender.AsyncAppender。二是看线程创建时机——用jcmd pid VM.native_memory看不到线程创建栈但可以在启动参数上加-XX:UnlockDiagnosticVMOptions -XX:ShowMessageBoxOnError之类的辅助手段或者更简单粗暴直接 grep 依赖版本找出哪些组件会自建线程。4. 一套可以照着抄的排查链路4.1 第一步永远是定位增长源不是修复排查这类问题的顺序我踩过几次坑之后固定成了四步确认是否真的在增长两次快照对比确认增长的是哪一类线程线程名归类确认这类线程是谁创建的栈里的类 线程名约定确认它为什么没被回收池的生命周期 / max 配置 / 下游阻塞。跳过任一步直接改参数大概率是白改。我见过有同学看到线程多直接去把keepAliveTime从 60s 改到 5s结果只是让线程退得更快本质上池仍在无界扩张高峰期线程数峰值一点没降。4.2 用栈顶统计替代肉眼扫栈前面给的 awk 命令稍微加工一下可以做成一个能反复用的小脚本#!/bin/bash # dump_top.sh 用法: ./dump_top.sh dump.txt DUMP$1 echo 线程总数 grep -c ^ $DUMP echo 状态分布 grep -o java.lang.Thread.State: [A-Z_]* $DUMP | sort | uniq -c | sort -rn echo TIMED_WAITING 栈顶 TOP20 grep -A 1 State: TIMED_WAITING $DUMP \ | grep -E ^\sat \ | awk {print $2} \ | sed s/(.*// \ | sort | uniq -c | sort -rn | head -20 echo 线程名前缀分布 TOP20 grep ^ $DUMP \ | awk -F {print $2} \ | sed -E s/-?[0-9]$// \ | sort | uniq -c | sort -rn | head -20跑一次不到两秒比人工翻几千行栈快得多而且不会漏。这个脚本我基本每次排查都会先跑一遍。4.3 判断是线程自己的问题还是下游把它堵住了这一步是最容易搞错的地方。线程数多不一定意味着线程池有问题也可能是下游把线程全都堵住了导致池不得不扩容。举个真实例子一个接口调用下游 HTTP 服务客户端超时设成了 30 秒本地线程池core10、max100。下游某天变慢响应时间从 50ms 涨到 3 秒。线程池的吞吐从 200 QPS 掉到个位数任务在队列里堆积池开始扩容到 100 个线程。这时候你看到的线程栈是什么样如果下游是用阻塞式 HTTP 客户端那会是RUNNABLE卡在SocketInputStream.socketRead0如果用的是带超时的Future.get(timeout)那就是TIMED_WAITING卡在Future.get。这两种栈和线程池空闲线程等过期的栈长得完全不同一眼能分。关键判据是栈里有没有业务代码或者第三方调用栈夹在中间。纯粹的getTask - poll只有三帧中间夹了业务逻辑的一定是被堵住的。4.4 顺手确认线程栈容量和系统上限线程数高的时候顺手确认几件事能避免把资源问题误判成代码问题# 进程的实际线程数比 jstack 更可靠 ls /proc/$PID/task | wc -l # 进程资源限制 cat /proc/$PID/limits | grep -i processes\|stack # 系统级线程上限 cat /proc/sys/kernel/threads-max ulimit -u-Xss默认 1MB每个线程一份。如果池上限定到 512 而-Xss是默认值光栈空间就是 512MB 的虚拟地址预留。真遇到pthread_create failed或者unable to create new native thread先看ulimit -u和物理内存再回头看代码。提示高并发场景下把-Xss调到 256KB 到 512KB 是常规操作业务代码只要没有超深递归基本不会有影响。这一条能直接把线程栈的开销砍掉一半以上。5. 队列、线程数和存活时间三个真正管用的旋钮5.1 队列选型决定了线程的阻塞形态线程池的阻塞队列选择这个问题的答案其实取决于你想要什么样的排队语义。队列类型容量空闲线程状态适用场景SynchronousQueue0TIMED_WAITINGcached 池语义追求低延迟、无缓冲必须配硬上限ArrayBlockingQueue有界队列非空时WAITING空闲超时TIMED_WAITING需要削峰且能接受一定排队延迟LinkedBlockingQueue默认无界WAITINGtake 无限等CPU 密集型稳定负载禁止用于不可控任务量PriorityBlockingQueue无界WAITING有优先级需求但必须自己控制入队量DelayedWorkQueue无界TIMED_WAITINGawaitNanos定时任务专用一般不用手写SynchronousQueue和ArrayBlockingQueue的选择题本质是用线程换队列还是用队列换线程。前者响应快但线程多后者线程少但峰值延迟高。我的经验是对外接口用有界ArrayBlockingQueue配拒绝策略内部批处理用SynchronousQueue配CallerRunsPolicy两边都不至于失控。5.2 core 与 max 的关系直接决定你看到 WAITING 还是 TIMED_WAITING回到第一节的timed判定。给你一个反直觉的结论corePoolSize maximumPoolSize且不接受超 core 线程时池里所有空闲线程都会是WAITING永远不会有 TIMED_WAITING。corePoolSize maximumPoolSize且发生过扩容超出的那部分空闲线程一定是TIMED_WAITING。所以如果你特别不想看到一堆 TIMED_WAITING 告警最直接的办法就是把 core 和 max 设成相等——代价是彻底放弃弹性伸缩。这是个取舍不是标准答案。实际项目里更常见的做法是保留弹性但把 max 控制住。至于网上流传的最大线程数设成 JVM 剩余可用线程数这个说法是有问题的最大线程数不应该是JVM 还剩多少能用而应该是我算下来需要多少。按下面的公式估CPU 密集型线程数 CPU 核数 1 IO 密集型线程数 CPU 核数 × (1 平均等待时间 / 平均计算时间)举个例子8 核机器单个任务平均耗时 100ms其中纯 CPU 时间 10ms其余 90ms 在等 IO那么目标线程数大约是8 × (1 90/10) 80。再留 50% 余量maximumPoolSize定在 120 左右是合理的。这个数远小于JVM 剩余线程数而且能解释得清楚——这比拍脑袋定 2000 靠谱得多。5.3 keepAliveTime 与 allowCoreThreadTimeOut 的取舍keepAliveTime默认 60 秒。它在两个地方起作用一是控制超 core 线程的存活时长二是当allowCoreThreadTimeOut(true)时控制所有线程的存活时长。我的经验值突发型流量有明确波峰波谷keepAliveTime设 10~30 秒让富余线程快速退场平稳型流量保持 60 秒默认值就行反正池也不会大幅扩容极低频任务几分钟一次allowCoreThreadTimeOut(true)keepAliveTime设 30 秒可以把空闲线程数压到 0。allowCoreThreadTimeOut(true)这个开关知道的人不多但它在低频但要求低延迟的场景里很好用。注意开了它之后所有线程都是 TIMED_WAITING这时候去数 TIMED_WAITING 就没有意义了得靠getPoolSize()来判断。提醒allowCoreThreadTimeOut(true)之后池在线程数降到 0 时如果有新任务到来需要重新创建线程会有一次创建开销。对延迟极度敏感的场景比如 P99 要求 1ms不建议开。5.4 线程工厂和命名是排查能力的下限这一条看起来是小事实际上是所有排查工作的地基。不给线程起名字你就只能在几千行栈里靠肉眼找。自定义线程工厂只多写几行public class NamedThreadFactory implements ThreadFactory { private final AtomicInteger seq new AtomicInteger(1); private final String prefix; private final boolean daemon; public NamedThreadFactory(String prefix, boolean daemon) { this.prefix prefix; this.daemon daemon; } Override public Thread newThread(Runnable r) { Thread t new Thread(r, prefix - seq.getAndIncrement()); t.setDaemon(daemon); t.setUncaughtExceptionHandler((thread, ex) - log.error(uncaught exception in {}, thread.getName(), ex)); return t; } }命名规范我习惯用业务域-用途-序号比如order-async-1、report-export-3。这样在jstack里grep ^order-async一下就能把某个业务池的所有线程捞出来数量、状态一目了然。同时配合setUncaughtExceptionHandler还能避免线程因为未捕获异常悄悄退出导致池内重新创建线程。6. C 线程池里的同一类现象6.1 std::condition_variable::wait_for 的等价语义热搜词里出现了C 线程池顺手把这一侧也补上。C 标准库没有内置线程池std::thread也不管复用。大家手写的线程池取任务的循环一般长这样void workerLoop() { while (true) { std::functionvoid() task; { std::unique_lockstd::mutex lock(mtx_); // 等新任务或者等停止信号最多等 keepAliveMs bool ok cv_.wait_for(lock, std::chrono::milliseconds(keepAliveMs_), [this] { return stop_ || !tasks_.empty(); }); if (stop_ tasks_.empty()) return; if (!ok) { // 超时这是富余线程退出池子的时机 if (threadCount_ coreSize_) { --threadCount_; return; } continue; } task std::move(tasks_.front()); tasks_.pop(); } task(); } }wait_for带谓词的版本语义上等价于 Java 里的workQueue.poll(keepAliveTime, NANOSECONDS)——超时返回false正好可以用来实现超出核心数的线程自动退出。所以 C 侧如果也看到大量线程卡在等待上排查思路和 Java 是一模一样的。6.2 用 gdb 或 pstack 看 C 侧的等待栈C 没有jstack替代品是gdb和pstack。最常用的一条命令gdb -p pid -batch -ex thread apply all bt bt.txt在输出里搜pthread_cond_timedwait或者pthread_cond_wait就能看到所有在条件变量上等待的线程。如果用的标准库较新std::condition_variable::wait_for内部会对齐单调时钟语义不会因为系统时间被校正而提前或延后唤醒这一点和 Java 的parkNanos是一致的。想看每个线程的 CPU 占用用top -H -p pid把线程 ID 转成十六进制printf %x\n tid再去bt.txt里找。找出占 CPU 最高的那个线程栈往往就能看到热点在哪。6.3 C 里更容易踩的两个坑第一个坑是忙轮询。有些人为了省事把取任务写成while (true) { if (auto task popTask()) { task(); } else std::this_thread::sleep_for(std::chrono::milliseconds(1)); }这会把线程全都卡在nanosleep上虽然也是等待但每秒唤醒上千次白白吃掉 CPU而且取任务的延迟随机在 0~1ms 之间浮动。正确做法就是上面用条件变量加谓词的写法。第二个坑是丢唤醒。如果wait_for不写谓词只用if (tasks_.empty()) cv_.wait_for(...)就可能踩到虚假唤醒——条件变量被唤醒时任务队列仍然是空的代码却继续往下走直接访问tasks_.front()。谓词版本内部是个while循环天然防住这个问题。这个坑在 C 里比 Java 更容易踩因为 Java 的poll是封装好的业务代码碰不到这层。7. 上线前把线程数这件事变成可观测的7.1 把池的关键指标吐出来线程数告警之所以总是来得突然是因为绝大多数项目根本没暴露线程池的运行时指标。ThreadPoolExecutor已经提供了完整的 getter包一层就能上报public class MonitoredThreadPool extends ThreadPoolExecutor { private final String poolName; public MonitoredThreadPool(String poolName, int core, int max, long keepAlive, TimeUnit unit, BlockingQueueRunnable queue, ThreadFactory factory, RejectedExecutionHandler handler) { super(core, max, keepAlive, unit, queue, factory, handler); this.poolName poolName; } public void report() { log.info(pool{} size{} active{} queue{} completed{} largest{}, poolName, getPoolSize(), getActiveCount(), getQueue().size(), getCompletedTaskCount(), getLargestPoolSize()); } }getPoolSize()是当前线程数getLargestPoolSize()是历史峰值线程数——后者特别有用它能告诉你这个池允许长到多大比看瞬时值更能反映配置是否合理。如果峰峰值只有 20而你的maximumPoolSize设了 500那这个配置就是虚的。上报频率建议 30 秒到 1 分钟一次接到现有监控体系里Prometheus 的 Gauge、或者日志里定时打点都行。有了历史曲线再回看那天半夜线程数两千就不再是悬案了。7.2 线程数和栈大小的容量账最后算一笔账把线程池的容量约束落到具体数字上。假设一台 8 核 16GB 的机器上跑着一个 Java 进程-Xss默认 1MB如果池上限 500虚拟地址空间预留 500MB堆配 8GB剩下的给元空间、直接内存、代码缓存和线程栈如果再叠加上本地线程GC 线程、编译器线程、JIT 线程和其他组件的线程实际线程数上限要留 20% 的余量。所以 16GB 的机器上把单个进程的线程数控制在 800 以内是个比较稳妥的区间。超出这个量级与其继续加线程不如回头看看队列深度、下游超时、批处理粒度这些更根本的东西——加线程通常只是把问题从队列搬到了线程栈上。7.3 贴在工位上的检查清单我把这套流程的前置检查整理成了一份清单每次新接入一个线程池就过一遍corePoolSize和maximumPoolSize分别是多少两者不等时是否清楚会产生TIMED_WAITING队列是什么类型、有没有容量上限满了之后的拒绝策略是什么keepAliveTime是多少是否开了allowCoreThreadTimeOut线程工厂有没有命名、有没有设置异常处理器池的生命周期由谁管理容器关闭时是否会调shutdown池的关键指标有没有上报告警阈值是按getPoolSize()还是按队列深度设的全局搜一遍Executors.new确认没有方法内建池。这份清单里最容易被忽略的是最后两条。前者关系到问题能不能被发现后者关系到问题会不会被创造出来。说句实在的TIMED_WAITING这个状态本身没什么可怕的它反而是线程池、定时器、连接池这些组件能高效运转的前提——没有它所有空闲线程就只能靠自旋或者 sleep 硬撑着。真正需要警惕的只有三件事数量在持续涨、栈里夹着业务代码、线程名重复出现大数字。这三条里只要中了一条就值得顺着上面那套流程走一遍剩下的交给配置和代码去改就行。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Mac浏览器下载文件名乱码:从Content-Disposition到修复 2026/10/1 5:00:45

Mac浏览器下载文件名乱码:从Content-Disposition到修复

1. 乱码不是玄学:先把「乱」分成三类Mac 浏览器下载的文件名总是「乱码」,这件事我被不同的人问过不下十次。最早我以为是个别网站的问题,直到有次自己用 Safari 从公司内部系统下载一份带中文名的 PDF,落盘之后变成了–‡‹•.pd…

阅读更多 →
AI微服务开发平台:从Demo到生产的企业级架构实战 2026/10/1 5:00:45

AI微服务开发平台:从Demo到生产的企业级架构实战

1. 为什么“AI 微服务开发平台”会成为企业落地的刚需1.1 从“能跑通 Demo”到“能扛住生产”之间的鸿沟过去一年多,我参与过好几个企业内部的 AI 应用项目,从智能客服、文档问答到流程自动化,几乎每个项目都经历过同一个尴尬阶段&#xff1a…

阅读更多 →
基于VOC格式的水泥泵车目标检测数据集训练与避坑指南 2026/10/1 5:00:44

基于VOC格式的水泥泵车目标检测数据集训练与避坑指南

简介:面向目标检测学习者和工程车辆识别开发者,这是一个Pascal VOC格式的水泥泵车检测数据集。共包含604张图片和604个对应的XML标注文件,标注类别为单一的水泥泵车,由标注工具画矩形框完成,框总数为626个。数据源自视…

阅读更多 →
PCL2启动器全指南:从下载安装到Forge/Fabric与Mod配置 2026/10/1 5:00:38

PCL2启动器全指南:从下载安装到Forge/Fabric与Mod配置

PCL2启动器(Plain Craft Launcher 2)是我在 Windows 上玩 Minecraft Java 版的主力启动器,没有之一。上周末帮朋友远程整理电脑,我在一台 Win11 26H2 的笔记本上,又把官网下载、解压安装、微软账号登录、配 Java、装 F…

阅读更多 →
VOC格式水泥泵车数据集训练全流程:转换、参数与避坑实战 2026/10/1 5:00:38

VOC格式水泥泵车数据集训练全流程:转换、参数与避坑实战

简介:面向目标检测任务研发的VOC格式工程车辆数据集,聚焦水泥泵车识别场景,共包含604张原始图片与604个XML标注文件,另附1份说明文件,压缩包内共计1209个文件,大小约49.38MB。标注工作使用labelImg工具完成…

阅读更多 →
开源AI编程工具ZCode解析:终端CLI原理、实测与选型指南 2026/10/1 5:00:38

开源AI编程工具ZCode解析:终端CLI原理、实测与选型指南

最近在GitHub上逛的时候,发现ZCode开源的消息被顶了上来。评论区吵得挺热闹,有人说这是又一个Claude Code类的AI编程工具,有人担心它“偷代码”,更多的人一脸懵——ZCode到底是什么?为什么大家都在讨论?我没…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉