继承 ThreadPoolExecutor 时应重写 beforeExecute 和 afterExecute 方法,用 AtomicInteger 在任务执行前后增减计数,确保活跃线程数准确;避免轮询 getActiveCount(),因其为过期快照且引发锁竞争;指标上报需异步缓冲,防止阻塞 worker 线程。

怎么继承 ThreadPoolExecutor 并拿到活跃线程数
直接重写 beforeExecute 和 afterExecute 是最轻量、最可靠的方式。别试图在外部轮询 getActiveCount(),那会漏掉瞬时峰值,也增加锁竞争。
关键点在于:活跃线程数只在任务真正开始执行(进入 worker 线程)和结束执行时才准确变化,所以必须钩住这两个时机:
-
beforeExecute里对计数器 +1,代表一个任务已上 CPU -
afterExecute里 -1,注意要放在finally块里,否则异常任务会导致计数漂移 - 用
AtomicInteger而不是普通int,避免并发修改丢失
为什么不能只靠 getActiveCount() 实时取值
getActiveCount() 返回的是当前正在运行任务的 worker 线程数,但它本身是基于内部锁统计的快照,**调用瞬间就可能过期**。尤其在高并发短任务场景下,两次调用间隔内可能已有数十个任务启停。
更严重的是:如果你在监控线程里反复调用它,会持续触发 ThreadPoolExecutor 内部的 mainLock,拖慢任务提交和执行路径 —— 监控反而成了性能瓶颈。
立即学习“Java免费学习笔记(深入)”;
所以真实指标必须是「事件驱动」的:任务启动/结束即更新,监控端只读取原子变量,零同步开销。
发送指标到监控系统要注意哪些坑
发送逻辑必须异步且带缓冲,否则 afterExecute 里网络 IO 或序列化失败会卡死 worker 线程,引发雪崩。
- 绝对不要在
beforeExecute/afterExecute里直接调用 HTTP 客户端或日志框架 - 推荐用无锁队列(如
ConcurrentLinkedQueue)暂存指标,另起一个守护线程批量上报 - 如果用 Micrometer,可注册一个
Gauge,让它定期读取你的AtomicInteger,而不是自己 push - 注意 JVM shutdown 时清空缓冲区,否则最后一分钟指标会丢失
一个最小可用的自定义线程池示例
下面这段代码能跑通、不丢数、不阻塞,且兼容 Spring 的 @Async:
public class MonitoredThreadPoolExecutor extends ThreadPoolExecutor {
private final AtomicInteger activeCount = new AtomicInteger(0);
private final MeterRegistry meterRegistry; // 如 Micrometer
public MonitoredThreadPoolExecutor(int corePoolSize, int maxPoolSize,
long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue,
MeterRegistry registry) {
super(corePoolSize, maxPoolSize, keepAliveTime, unit, workQueue);
this.meterRegistry = registry;
Gauge.builder("threadpool.active", activeCount, AtomicInteger::get)
.register(registry);
}
@Override
protected void beforeExecute(Thread t, Runnable r) {
super.beforeExecute(t, r);
activeCount.incrementAndGet();
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
try {
super.afterExecute(r, t);
} finally {
activeCount.decrementAndGet();
}
}
}
重点看 finally 和 Gauge 注册方式 —— 这决定了你看到的数字是不是真能反映负载。
复杂点在于:如果线程池被多次包装(比如 Spring 的 ThreadPoolTaskExecutor 包了一层),你要确保钩子函数没被绕过;还有,拒绝策略触发时任务根本不会进 beforeExecute,这部分活跃度无法捕获,得单独记日志或埋点。










