为什么需要监控
线程池在业务系统应该都有使用到,帮助业务流程提升效率以及管理线程,多数场景应用于大量的异步任务处理。
虽然线程池提供了我们许多便利,但也并非尽善尽美,比如下面这些问题就无法很好解决。
- 线程池随便定义,线程资源过多,造成服务器高负载。
- 线程池参数不易评估,随着业务的并发提升,业务面临出现故障的风险。
- 线程池任务执行时间超过平均执行周期,开发人员无法感知。
- 线程池任务堆积,触发拒绝策略,影响既有业务正常运行。
- 当业务出现超时、熔断等问题时,因为没有监控,无法确定是不是线程池引起。
- 原生线程池不支持运行时变量的传递,比如 MDC 上下文遇到线程池就 GG。
- 无法执行优雅关闭,当项目关闭时,大量正在运行的线程池任务被丢弃。
- 线程池运行中,任务执行停止,怀疑发生死锁或执行耗时操作,但是无从下手
如何切入
接口和抽象类总览
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);
}
- corePoolSize:核心线程数量,线程池维护线程的最少数量
- maximumPoolSize:线程池维护线程的最大数量,超过这个数会执行饱和策略
- keepAliveTime:线程池除核心线程外的其他线程的最长空闲时间,超过该时间的空闲线程会被销毁
- unit:keepAliveTime的单位,TimeUnit中的几个静态属性:NANOSECONDS、MICROSECONDS、MILLISECONDS、SECONDS
- workQueue:线程池所使用的任务缓冲队列,超过corePoolSize小于maximumPoolSize的任务会存在这个队列中
- threadFactory:线程工厂,用于创建线程,一般用默认的即可
- handler:线程池对拒绝任务的处理策略
比较基本的知识,不再展开(对线程池不够了解点这里)
很多同学对线程池的了解也就基于此处了,如果认真查看这个类,你会发现一下几个重要成员变量:
- largestPoolSize:线程池达到过的最大池大小
- completedTaskCount:已完成任务的计数器。仅在工作线程终止时更新
- allowCoreThreadTimeOut:如果为 false(默认值),则核心线程即使在空闲时也保持活动状态。如果为 true,则核心线程使用 keepAliveTime 超时等待工作。参考:线程池的核心线程会销毁吗?
- corePoolSize:核心池大小是保持活动状态的最小工作线程数(不允许超时等),除非设置了 allowCoreThreadTimeOut,在这种情况下,最小值为零。(这个值就算核心线程被销毁了也不会改变)
- maximumPoolSize:线程池维护线程的最大数量,超过这个数会执行饱和策略
- 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