前几篇我们解决了两个问题:
第 6 篇:任务怎么正确提交(submit / execute / Future)
第 7 篇:异常为什么会消失,以及如何兜底
但即便你都做对了,线程池依然可能出现一种最危险的状态:
线程池没有报错、没有拒绝、服务也没挂,
但请求越来越慢,延迟越来越高,最终拖垮系统。
这就是——不可观测的线程池。
一、先给结论:线程池必须“看得见”
一句工程级结论:
没有监控的线程池,本质上是一个黑盒;
黑盒在生产环境里,迟早会出事故。
线程池的问题往往不是“瞬间爆炸”,而是:
队列慢慢变长
活跃线程长期打满
任务执行时间越来越长
拒绝策略迟迟不触发
这类问题,如果你不主动监控,等用户告诉你就已经晚了。
二、ThreadPoolExecutor 已经给了你监控接口(只是你没用)
ThreadPoolExecutor天然就是一个可观测对象。
核心监控指标(必须掌握)
int getCorePoolSize(); int getMaximumPoolSize(); int getPoolSize(); int getActiveCount(); long getTaskCount(); long getCompletedTaskCount(); BlockingQueue<Runnable> getQueue(); boolean isShutdown(); boolean isTerminated();你不用猜,线程池状态 JVM 都知道。
三、最关键的 6 个指标(生产必看)
1️⃣ activeCount(活跃线程数)
pool.getActiveCount()- 接近 max → 线程池压力大
- 长期等于 max → 任务执行慢 or 线程数太小
2️⃣ queue size(队列长度)
pool.getQueue().size()- 队列持续增长 → 生产速度 > 消费速度
- 这是雪崩前最明显的信号
3️⃣ completedTaskCount(已完成任务)
pool.getCompletedTaskCount()- 增长是否平稳?
- 是否突然停滞?
4️⃣ taskCount(总任务数)
pool.getTaskCount()配合 completedTaskCount,可以判断:
任务是否在“进多出少”
5️⃣ poolSize vs corePoolSize
pool.getPoolSize()- poolSize 一直等于 core → 线程扩容没发生
- poolSize 快速膨胀 → 突发压力
6️⃣ rejected 次数(你必须自己记录)
JDK 不会帮你统计拒绝次数,
生产中一定要在 RejectedExecutionHandler 里埋点。
四、一个最小可用的监控方法(立刻能用)
logState:你可以直接放进 ThreadPoolManager
public static void logState(ThreadPoolExecutor pool, String name) { System.out.printf( "[POOL-%s] core=%d, max=%d, pool=%d, active=%d, queue=%d, completed=%d%n", name, pool.getCorePoolSize(), pool.getMaximumPoolSize(), pool.getPoolSize(), pool.getActiveCount(), pool.getQueue().size(), pool.getCompletedTaskCount() ); }定时打印:
ScheduledExecutorService monitor = Executors.newSingleThreadScheduledExecutor(); monitor.scheduleAtFixedRate(() -> logState(ioPool, "IO"), 0, 5, TimeUnit.SECONDS);这一步,就已经让你的线程池不再是黑盒。
五、如何判断“线程池正在慢性死亡”?
下面这几种组合信号,任何一个出现都要警惕:
🚨 信号 1:activeCount 长期 == maxPoolSize
👉 线程不够 or 任务太慢
🚨 信号 2:queue size 持续增长
👉 明确的背压失败信号(消费跟不上)
🚨 信号 3:completedTaskCount 增长变慢
👉 单个任务耗时变长(下游慢、锁竞争、IO 阻塞)
🚨 信号 4:拒绝策略突然频繁触发
👉 系统已进入保护状态(但用户已感知)
🚨 信号 5:poolSize 始终达不到 max
👉 队列太大(典型 LinkedBlockingQueue 无界坑)
六、把监控和“动作”结合(不是只看)
监控不是为了看,而是为了触发决策。
示例:队列过长,直接报警 / 降级
if (pool.getQueue().size() > 1000) { // 报警 / 限流 / 丢弃低优先级任务 }示例:active 长期满载,触发告警
if (pool.getActiveCount() == pool.getMaximumPoolSize()) { // 发告警:线程池已满载 }七、和第 5 篇“生产级封装”的结合方式
你第五篇的 ThreadPoolManager,应该至少做到:
✔ 统一创建线程池
✔ 统一命名
✔ 统一异常处理
✔统一监控出口(logState / metrics)
你可以对外暴露:
ThreadPoolStats snapshot();
然后:
- 打日志
- 接 Prometheus / Micrometer
- 上报到监控平台
八、一个重要认知:线程池问题 ≠ CPU 问题
很多人误判:
- CPU 不高 → 没问题
- 内存够 → 没问题
但线程池的真实问题往往是:
- IO 慢
- 锁竞争
- 下游接口慢
- 队列堆积
👉线程池是“吞吐瓶颈”,不是资源瓶颈。
九、本篇总结
- ThreadPoolExecutor 天生可观测,但默认没人看
- activeCount、queue size 是最关键的预警信号
- 队列持续增长 = 系统正在慢性死亡
- 拒绝策略触发时,用户通常已经感知
- 生产线程池必须“可观测 + 可告警 + 可干预”