Java并发编程(二十二):Java线程池的监控


大家好,我是欧阳方超,公众号同名。

在这里插入图片描述

1 概述

可监控的线程池是指在实际生产环境中,对ThreadPoolExecutor或其子类进行增强,让开发/运维人员能够实时感知线程池的运行健康状态、及时发现问题、进行告警,并支持后续调优的线程池实现。

2 JDK原生线程池状态获取

其实,JDK提供的ThreadPoolExecutor自带了丰富的状态查询API。要想监控它,只需要定时去获取这些数据即可。可获取到的核心指标包括:

  • getPoolSize():当前线程池中的实际线程数。
  • getActiveCount():当前正在执行任务的线程数(最核心的负载指标)。
  • getQueue().size():当前排队等待执行的任务数(积压指标,决定是否会触发拒绝策略)。
  • getCompletedTaskCount():已完成的任务总数。

2.1 轻量级监控器实现

这里实现一个轻量级监控器,实现思路是通过开启一个后台定时任务(ScheduledExecutorService),每隔几秒拉取一次核心业务线程池的状态并打印到日志中。


import java.util.concurrent.*;

public class ThreadPoolExecutorDemo {
    public static void main(String[] args) {
        //1.初始化核心业务线程池
        ThreadPoolExecutor orderPool = new ThreadPoolExecutor(
                5,
                10,
                60,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(100),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );

        //2.启动一个监控线程,定时打印监控指标
        ScheduledExecutorService monitorThread = Executors.newSingleThreadScheduledExecutor();
        monitorThread.scheduleAtFixedRate(() -> {
            System.out.printf("[监控采集] 核心线程数:%d,活动线程数:%d,最大线程数:%d,队列中的任务数:%d,已完成任务数:%d%n",
                    orderPool.getCorePoolSize(),
                    orderPool.getActiveCount(),
                    orderPool.getMaximumPoolSize(),
                    orderPool.getQueue().size(),
                    orderPool.getCompletedTaskCount()
                    );
        }, 0, 2, TimeUnit.SECONDS);

        //3.模拟并发提交任务
        for (int i = 0; i < 20; i++) {
            orderPool.execute(() -> {
                try {
                    Thread.sleep(1000);//模拟任务耗时
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
        }
    }
}

上面的程序中,首先创建了一个核心业务线程池orderPool,接着创建了一个用于监控orderPool线程池指标的监控线程,该线程每两秒输出一次orderPool线程池的核心线程数、活动线程数等指标,接着主线程让orderPool线程池中的线程执行20个任务,程序的输出如下:

[监控采集] 核心线程数:5,活动线程数:0,最大线程数:10,队列中的任务数:0,已完成任务数:0
[监控采集] 核心线程数:5,活动线程数:5,最大线程数:10,队列中的任务数:10,已完成任务数:5
[监控采集] 核心线程数:5,活动线程数:5,最大线程数:10,队列中的任务数:0,已完成任务数:15
[监控采集] 核心线程数:5,活动线程数:0,最大线程数:10,队列中的任务数:0,已完成任务数:20

注意,上面的程序并不会停止,即使orderPool执行完了20个任务,因为所创建的orderPool、monitorThread都是非守护线程,即用户线程,jvm会等待所有非守护进程结束后退出,如果想让上面的程序优雅结束,可以这样调整,把monitorThread设置为守护线程,在for循环后面加一行代码,关闭orderPool,即orderPool.shutdown(),关闭之后orderPool不再接收新任务,但会把正在执行的任务以及队列中等待的任务全部执行完毕,即会保证那20个任务执行完毕,此时jvm进程中只剩monitorThread一个守护线程了,而jvm又不会等待守护线程执行结束后退出,因此调整之后jvm进程会退出。
这种原生方案有点是零依赖、轻量级、开箱即用,缺点是只能打印日志,无法对接现代的可观测系统(如Prometheus),缺少报警机制,只能“看”不能“动”。

3 微服务监控体系(Micrometer/Spring Boot Actuator)

在Spring Boot生态中,工业级的监控标准是基于Micrometer,它相当于指标界(Metrics)的SLF4J。通过Micrometer的ExecutorServiceMetrics,可以将JDK的线程池包装起来,将各项指标无缝暴露给Prometheus,最终在Grafana上画出漂亮的仪表盘。
下面的示例展示注册线程池到全局注册表,只需在Spring Boot的配置类中对线程池进行包装注册:

import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.*;

@Configuration
public class ThreadPoolConfig {

    @Bean
    public ExecutorService orderThreadPool(MeterRegistry meterRegistry) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                10, 20, 60, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(500),
                new ThreadPoolExecutor.AbortPolicy()
        );

        // 【核心魔法】将线程池注册到 Micrometer,命名为 "order_pool"
        return ExecutorServiceMetrics.monitor(meterRegistry, executor, "order_pool");
    }
}

程序启动后,访问ip:port/actuator/prometheus端点,可以在页面上看到如下所示的时序数据:

# TYPE executor_active_threads gauge
executor_active_threads{application="userservice",name="clientOutboundChannelExecutor",} 0.0
executor_active_threads{application="userservice",name="brokerChannelExecutor",} 0.0
executor_active_threads{application="userservice",name="messageBrokerTaskScheduler",} 0.0
executor_active_threads{application="userservice",name="clientInboundChannelExecutor",} 0.0
executor_active_threads{application="userservice",name="order_pool",} 0.0

注意,在Spring Boot工程中需要引入如下依赖:

<!-- 1. Spring Boot 官方监控组件 (这会传递引入 micrometer-core) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <!-- 2. Micrometer 桥接 Prometheus (负责把底层指标转换为 Prometheus 认识的拉取格式) -->
        <dependency>
            <groupId>io.micrometer</groupId>
            <artifactId>micrometer-registry-prometheus</artifactId>
        </dependency>

yaml配置文件中需要加入如下内容:

management:
  endpoints:
    web:
      exposure:
        #暴露所有监控端点可以写"*",生产环境为了安全可以只填"health,prometheus,metrics"
        include: prometheus
  metrics:
    tags:
       #强烈建议加上应用名标签,方便在 Grafana 中区分是哪个服务
      application: userservice
    export:
      prometheus:
        enabled: true #开启Prometheus格式输出

Micrometer方法的优点是可以完美接入云原生Prometheus+Grafana体系,但缺点是依然是静态的,当从大屏上发现队列打满、频繁报警时,采取的措施也只能是重新发版改代码、改配置文件后重启服务。

4 开源动态可监控线程池框架(强烈推荐生产环境使用)

“监控”不仅是为了看,更是为了“治”。
动态线程池的出现一并解决了“看”和“治”的问题,这是业界在实践中提出的一个最佳实践概念,它的核心架构思想是:

  1. 全面接管:侵入底层接管Spring容器中的所有线程池。
  2. 配置中心驱动:将corePoolSize、maximumPoolSize、队列容量等参数放到Nacos或Apollo等配置中心。
  3. 动态感知:监听配置中心的变更事件,通过executor.setCorePoolSize()等底层API实时动态刷新,无需重启服务器。
  4. 开箱即用的告警:内置企业微信、钉钉、飞书的机器人报警通知机制(队列满了直接发企业微群)。
    目前主流的成熟开源方案:
    Hippo4j(opengoofy/hippo4j):支持动态变更参数、实时监控、告警,无需改代码,支持多种配置中心,如Nacos、Apollo等。
    Dynamic-TP:支持动态线程池、监控告警、适配Dubbo/Tomcat等框架线程池。

4.1 以示例展示Dynamic-TP落地全过程

引入依赖(基于Nacos)

<dependency>
    <groupId>org.dromara.dynamictp</groupId>
    <artifactId>dynamic-tp-spring-boot-starter-nacos</artifactId>
    <version>相应版本</version>
</dependency>

在Nacos中声明配置
把原本写死在Java代码中的参数,全部转移到配置文件中:

spring:
  dynamic:
    tp:
      enabled: true
      executors:
        - threadPoolName: dtpExecutor-order-pool
          threadPoolAliasName: 订单核心线程池
          executorType: threadPoolExecutor
          corePoolSize: 10
          maximumPoolSize: 20
          queueType: VariableLinkedBlockingQueue # DTP 提供的支持动态变长队列
          queueCapacity: 500
          rejectedHandlerType: AbortPolicy
          notifyItems: # 告警配置
            - type: capacity     # 队列容量达到阈值报警
              threshold: 80      # 阈值 80%
              platforms: [wechat] # 推送到企业微信机器人
            - type: reject       # 触发拒绝策略报警
              threshold: 1
              platforms: [wechat]

在代码中一键获取线程池并使用
Java代码变得极其清爽,只需要通过DTP的注册中心根据名称获取即可,如下示例:

import org.dromara.dynamictp.core.DtpRegistry;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.Executor;

@RestController
public class OrderController {

    @GetMapping("/submitOrder")
    public String submitOrder() {
        // 从 DTP 注册表中获取被动态托管的线程池
        Executor orderPool = DtpRegistry.getExecutor("dtpExecutor-order-pool");
        
        orderPool.execute(() -> {
            System.out.println("处理核心订单逻辑, 当前线程: " + Thread.currentThread().getName());
        });
        
        return "提交成功";
    }
}

加入订单接口流量突增:
触发告警:企业微信群立刻弹窗告警:“⚠️ 【订单核心线程池】队列容量已达 85%,即将触发拒绝策略!”
动态调参:此时直接登录Nacos控制台,将maximumPoolSize从20改为50,queueCapacity 改为 1000
平滑过渡:保存配置,系统吞吐量翻倍,系统平滑度过流量洪峰。

5 总结

可监控的线程池,本质上是对ThreadPoolExecutor的可观测性做了增强。实际开发中对线程池的使用应当遵循三个层次:

  1. 代码中导出new Thread()或Executors.newFixedThreadPool()。
  2. 自定义ThreadPoolExecutor,做物理隔离,并用Micrometer对接Prometheus做大屏展示。
  3. 全面拥抱Dynamic-TP、Hippo4j等可观测可治理框架,将参数治理权移交至配置中心,现秒级的扩缩容与多维度的报警护航。
    我是欧阳方超,把事情做好了自然就有兴趣了,如果你喜欢我的文章,欢迎点赞、转发、评论加关注。我们下次见。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值