java怎么做一个轻量级的线程池监控?

为什么需要监控

线程池在业务系统应该都有使用到,帮助业务流程提升效率以及管理线程,多数场景应用于大量的异步任务处理。

虽然线程池提供了我们许多便利,但也并非尽善尽美,比如下面这些问题就无法很好解决。

  1. 线程池随便定义,线程资源过多,造成服务器高负载。
  2. 线程池参数不易评估,随着业务的并发提升,业务面临出现故障的风险。
  3. 线程池任务执行时间超过平均执行周期,开发人员无法感知。
  4. 线程池任务堆积,触发拒绝策略,影响既有业务正常运行。
  5. 当业务出现超时、熔断等问题时,因为没有监控,无法确定是不是线程池引起。
  6. 原生线程池不支持运行时变量的传递,比如 MDC 上下文遇到线程池就 GG。
  7. 无法执行优雅关闭,当项目关闭时,大量正在运行的线程池任务被丢弃。
  8. 线程池运行中,任务执行停止,怀疑发生死锁或执行耗时操作,但是无从下手

如何切入

接口和抽象类总览

Executor 接口: 是线程池的核心接口,定义了一个执行任务的方法 void execute(Runnable command)。

ExecutorService 接口: 扩展了 Executor 接口,提供了更丰富的任务提交和管理功能,如任务提交、关闭线程池、获取 Future 对象等

AbstractExecutorService 抽象类: 实现了 ExecutorService 接口的骨架,为一些基本方法提供了默认实现,简化了自定义线程池的开发。

ScheduledExecutorService 接口: 继承了 ExecutorService 接口,提供了支持任务定时执行和周期性执行的功能。

ThreadPoolExecutor是线程池的核心,继承AbstractExecutorService类,先看一下构造函数

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}

  1. corePoolSize:核心线程数量,线程池维护线程的最少数量
  2. maximumPoolSize:线程池维护线程的最大数量,超过这个数会执行饱和策略
  3. keepAliveTime:线程池除核心线程外的其他线程的最长空闲时间,超过该时间的空闲线程会被销毁
  4. unit:keepAliveTime的单位,TimeUnit中的几个静态属性:NANOSECONDS、MICROSECONDS、MILLISECONDS、SECONDS
  5. workQueue:线程池所使用的任务缓冲队列,超过corePoolSize小于maximumPoolSize的任务会存在这个队列中
  6. threadFactory:线程工厂,用于创建线程,一般用默认的即可
  7. handler:线程池对拒绝任务的处理策略

比较基本的知识,不再展开(对线程池不够了解点这里

很多同学对线程池的了解也就基于此处了,如果认真查看这个类,你会发现一下几个重要成员变量:

  1. largestPoolSize:线程池达到过的最大池大小
  2. completedTaskCount:已完成任务的计数器。仅在工作线程终止时更新
  3. allowCoreThreadTimeOut:如果为 false(默认值),则核心线程即使在空闲时也保持活动状态。如果为 true,则核心线程使用 keepAliveTime 超时等待工作。参考:线程池的核心线程会销毁吗?
  4. corePoolSize:核心池大小是保持活动状态的最小工作线程数(不允许超时等),除非设置了 allowCoreThreadTimeOut,在这种情况下,最小值为零。(这个值就算核心线程被销毁了也不会改变
  5. maximumPoolSize:线程池维护线程的最大数量,超过这个数会执行饱和策略
  6. workQueue:线程池所使用的任务缓冲队列,超过corePoolSize小于maximumPoolSize的任务会存在这个队列中

对第四点的解释:如下代码,当线程结束了一个任务时,会判断需要维持的最小活动线程,当
allowCoreThreadTimeOut为true时,最小工作线程数是0,而不是corePoolSize为0;

if (runStateLessThan(c, STOP)) {
            if (!completedAbruptly) {
                int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
                if (min == 0 && ! workQueue.isEmpty())
                    min = 1;
                if (workerCountOf(c) >= min)
                    return; // replacement not needed
            }
            addWorker(null, false);
        }

初级程序员关心比较多的是corePoolSize、maximumPoolSize、workQueue的大小怎么设计最合理,这无可厚非,因为这就是最低层的原理,但是当我们工作中,面对文章开头的几个问题,诸君又该如何面对?

怎么做

直接上demo

BlockingQueue<Runnable> blockingQueue = threadPoolExecutor.getQueue();
        long rejectCount = threadPoolExecutor.getRejectCountNum();

        //checkPoolCapacityAlarm
        int queueSize = blockingQueue.size();
        int capacity = queueSize + blockingQueue.remainingCapacity();
        int capacityAlarmDivide = CalculateUtil.divide(queueSize, capacity);


        //checkPoolActivityAlarm
        int activeCount = threadPoolExecutor.getActiveCount();
        int maximumPoolSize = threadPoolExecutor.getMaximumPoolSize();
        int activityAlarmDivide = CalculateUtil.divide(activeCount, maximumPoolSize);


        StringBuilder sb = new StringBuilder("【线程池画像】\n");
        sb.append("name:").append(threadPoolExecutor.getThreadPoolId())
            .append("\n 最大线程数:").append(threadPoolExecutor.getMaximumPoolSize())
            .append("\n 核心线程数:").append(threadPoolExecutor.getCorePoolSize())
            .append("\n 活跃线程数:").append(threadPoolExecutor.getActiveCount())
            .append("\n 当前线程数:").append(threadPoolExecutor.getPoolSize())
            .append("\n 历史最大线程数:").append(threadPoolExecutor.getLargestPoolSize())
            .append("\n 活跃线比例:").append(activityAlarmDivide)
            .append("\n 列队容量:").append(capacity)
            .append("\n 队列使用容量:").append(queueSize)
            .append("\n 队列剩余容量:").append(blockingQueue.remainingCapacity())
            .append("\n 队列使用占比:").append(capacityAlarmDivide)
            .append("\n 拒绝的任务数:").append(rejectCount);

可以看到,除了线程池的几个核心参数,还添加了队列的使用情况

这样在线上的机器出现问题时,就可以第一时间获取以上线程池画像来定位问题;

如何监控

可以做定时任务发送企业微信消息

可以持久化到数据库,展示在后台web页面,或者接入Grafana

有没有拿来就能用的框架

如果你不想自己动手,可考虑使用hippo4j

创建一个线程池可以通过以下步骤实现: 1. 定义线程池的大小:确定线程池中线程的数量,根据需要和系统资源进行调整。 2. 创建线程池:创建一个包含指定数量线程的线程池对象。 3. 初始化线程池:为每个线程分配任务队列,并初始化其他必要的数据结构。 4. 提交任务:将任务提交到线程池中,可以使用线程池提供的方法将任务添加到任务队列中。 5. 线程执行任务:线程池中的线程会从任务队列中获取任务并执行。 6. 处理任务完成:线程执行完任务后,可以将结果返回给调用者或者进行其他处理。 7. 关闭线程池:当不再需要线程池时,需要关闭线程池,释放资源。 下面是一个简单的示例代码,演示如何使用Java的Executor框架创建一个线程池: ```java import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadPoolExample { public static void main(String[] args) { // 创建一个固定大小为5的线程池 ExecutorService executor = Executors.newFixedThreadPool(5); // 提交任务到线程池 for (int i = 0; i < 10; i++) { final int taskId = i; executor.submit(new Runnable() { public void run() { System.out.println("Task " + taskId + " is being executed."); } }); } // 关闭线程池 executor.shutdown(); } } ``` 这个示例中,我们使用`Executors.newFixedThreadPool()`方法创建一个固定大小为5的线程池。然后,我们使用`executor.submit()`方法提交了10个任务到线程池中。每个任务都会打印出自己的任务ID。最后,我们调用`executor.shutdown()`方法关闭线程池
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值