getCompletedTaskCount 返回的是线程池自创建以来成功执行完毕的任务总数,不包括被拒绝、取消或运行中的任务,是线程安全的近似值,仅适用于 ThreadPoolExecutor 及其子类。

getCompletedTaskCount 返回的到底是啥任务数
getCompletedTaskCount 返回的是该线程池**自创建以来成功执行完毕的任务总数**,不包括被拒绝、取消或仍在运行的任务。它只在 ThreadPoolExecutor 及其子类中可用,且是线程安全的——但注意:这个值不是实时快照,而是基于内部计数器的近似值,高并发下可能略微滞后(通常误差在个位数)。
常见误用是把它当“当前已完成数”来判断某批任务是否结束,结果发现数值没变——其实是因为任务还没真正 run() 完(比如卡在 I/O 或锁等待),或者你调用的是 submit() 返回的 Future,而 getCompletedTaskCount 统计的是 execute() 和 submit() 底层触发的 Runnable 执行完成数,和 Future 的 get() 状态无关。
- 只对
ThreadPoolExecutor有效,ForkJoinPool或第三方线程池(如 Netty 的EventLoopGroup)不支持 - 如果任务抛出未捕获异常,只要
run()方法返回了(哪怕异常终止),也算进这个计数 - 重启线程池实例后计数归零,无法跨生命周期累积
怎么安全地动态调整核心线程数与最大线程数
调用 setCorePoolSize(int) 和 setMaximumPoolSize(int) 是线程安全的,但效果取决于当前负载状态。关键不是“能不能设”,而是“设了之后会不会立刻起效”以及“会不会引发意外行为”。
比如把 corePoolSize 从 4 降到 2,已存在的空闲核心线程不会马上销毁,得等它们自然超时(由 keepAliveTime 控制);而如果此时队列里有积压任务,新来的任务仍可能触发新线程创建(只要没超 maximumPoolSize)。反过来,把 maximumPoolSize 调小,正在运行的超纲线程不会被强制中断,但后续拒绝策略会更早触发。
立即学习“Java免费学习笔记(深入)”;
- 调小
corePoolSize后,建议配合allowCoreThreadTimeOut(true),否则空闲核心线程永不回收 -
setMaximumPoolSize必须 ≥corePoolSize,否则抛IllegalArgumentException - 频繁调参(比如每秒改一次)会导致线程创建/销毁开销上升,反而降低吞吐,建议用指数退避或阈值触发机制控制调节频率
监控线程池状态不能只看 getCompletedTaskCount
单靠 getCompletedTaskCount 得不到运行中任务数、排队数、拒绝数这些关键指标,容易误判瓶颈位置。比如数值增长缓慢,你以为是线程不够,实际可能是任务全堵在 LinkedBlockingQueue 里,而线程池本身空闲着。
应该组合使用以下方法:
-
getActiveCount():当前正在执行任务的线程数(注意不是“活跃线程数”,而是“正跑着任务的线程数”) -
getQueue().size():等待执行的任务数量(对无界队列如LinkedBlockingQueue,这个值可能极大,慎用 size()) -
getTaskCount():已提交的总任务数(含已完成、运行中、排队中),减去getCompletedTaskCount()就是“尚未完成”的粗略数 - 自定义
RejectedExecutionHandler记录拒绝次数,因为ThreadPoolExecutor不提供内置拒绝计数器
如果用 Micrometer 或 Prometheus,优先走 ThreadPoolExecutorMetrics 这类封装,避免自己反复调用多个 getter 引发性能抖动。
动态调参时最容易被忽略的拒绝策略副作用
很多人调大 maximumPoolSize 来“缓解拒绝”,却忘了默认的 AbortPolicy 在拒绝时直接抛 RejectedExecutionException。一旦参数生效后突发流量打进来,原来能兜住的请求突然开始抛异常,上游服务没做重试或降级,就直接雪崩了。
更隐蔽的问题是:如果你用了 CallerRunsPolicy,调参后线程池压力下降,但这个策略会让调用线程自己执行任务,导致业务线程卡住,TP99 毛刺明显上升,而监控图表上 getActiveCount 却很低——因为执行者不是线程池里的线程。
- 修改线程池参数前,确认拒绝策略是否匹配业务容忍度(比如日志型任务可用
DiscardPolicy,支付类必须用AbortPolicy+ 全链路告警) -
CallerRunsPolicy下,getActiveCount()不包含调用方线程,别拿它算资源占用 - 所有动态调参操作,必须记录操作时间点和参数变更值,否则排查问题时分不清是代码改了还是配置变了
线程池不是开关,调参像调音——拧一个旋钮,其他频段也会共振。最麻烦的永远不是“怎么改”,而是“改完谁在用、用得对不对、错了怎么回滚”。










