概述
最近公司项目生产上遇到了一个线程池的问题,由于线程池的配置参数开发人员没有根据实际情况考量,拍脑袋想了一个,将核心线程数和队列数都设置的很大,实际生产提交的任务很多,导致出现CPU很高,影响了其他的一些业务接口。回过头,复盘发现,我们没有对生产的线程池任务做一些监控,比如该任务执行了多长时间,在队列中等待了多少时间等等,那么有什么线程池的编程范式可以监控线程池的任务执行情况呢?
目标
主要是为了实现下面的2个目标:
- 线程池中任务在队列中等待的时间
- 线程池中任务运行的时间
我们可以根据实际场景需求打印日志或者通过prometheus输出到监控系统中。
解决方案
我们可以采用装饰者设计模式,写一个线程池任务的包装类,公司项目中所有要提交的任务必须要使用这个包装类去提交任务,定义任务的包装类代码如下:
@Slf4j public class RunnableWrapper implements Runnable { // 实际要执行的线程任务 private Runnable task; // 线程任务被创建出来的时间 private long createTime; // 线程任务被线程池运行的开始时间 private long startTime; // 线程任务被线程池运行的结束时间 private long endTime; // 线程信息 private String taskInfo; // 当这个任务被创建出来的时候,就会设置他的创建时间 // 但是接下来有可能这个任务提交到线程池后,会进入线程池的队列排队 public RunnableWrapper(Runnable task, String taskInfo) { this.task = task; this.taskInfo = taskInfo; this.createTime = System.currentTimeMillis(); } // 当任务在线程池排队的时候,这个run方法是不会被运行的 // 但是当任务结束了排队,得到线程池运行机会的时候,这个方法会被调用 // 此时就可以设置线程任务的开始运行时间 @Override public void run() { this.startTime = System.currentTimeMillis(); // 此处可以通过调用监控系统的API,实现监控指标上报 // 用线程任务的startTime-createTime,其实就是任务排队时间 // 这边打印日志输出,也可以输出到监控系统中 log.info("任务信息: [{}], 任务排队时间: [{}]ms", taskInfo, startTime - createTime); // 接着可以调用包装的实际任务的run方法 task.run(); // 任务运行完毕以后,会设置任务运行结束的时间 this.endTime = System.currentTimeMillis(); // 此处可以通过调用监控系统的API,实现监控指标上报 // 用线程任务的endTime - startTime,其实就是任务运行时间 // 这边打印任务执行时间,也可以输出到监控系统中 log.info("任务信息: [{}], 任务执行时间: [{}]ms", taskInfo, endTime - startTime); } }
后续我们提交线程池任务统一使用该任务包装类。
使用案例
比如定义一个线程池如下, 核心线程数40, 最大线程数800,线程队列为500:
@Bean(destroyMethod = "shutdown") public ThreadPoolExecutor sumTaskThreadPool() { return new ThreadPoolExecutor(50, 800, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(500)); }
定义一个求和的任务:
public class SumRunnable implements Runnable { @SneakyThrows @Override public void run() { int sum = 0; for (int i = 0; i < 100000; i++) { sum = sum + i; } Thread.sleep(1000); } }
提交任务到线程池中:
@PostConstruct public void init() throws InterruptedException { // 提交1000个线程任务 for (int i = 0; i < 1000; i++) { // 定义求和任务 SumRunnable sumRunnable = new SumRunnable(); // 包装求和任务 RunnableWrapper runnableWrapper = new RunnableWrapper(sumRunnable, "求和线程任务" + i); // 提交任务 threadPoolExecutor.submit(runnableWrapper); Thread.sleep(100); } }
可以看到打印的结果如下图:
这样就可以打印出任务的排队时间和执行时间了。
总结
不仅runnable可以进行这样的包装,对于Callable任务也可以进行包装。定义这样的包装类简单,难点在于如何根据打印出的日志进行分析和调整线程池参数。另外就是如何推动公司的同事都使用这样的线程池使用范式也是一大问题。