一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情。
前言
为了提升系统的性能运行,节省资源开销,对于一些任务处理,大家习惯用线程池来实现任务的异步化。
但是线程池到底应该配置多大,各种参数(如:corePoolSize, maximumPoolSize, workQueue等)应该是多少,在不同的场景下,参数肯定不同。
常见的一些配置建议,如:
* 1. IO密集型的任务,并不是一直在执行任务,则应配置尽可能多的线程,如CPU核数*2, 或者CPU核数 /(1 - 阻系数)
* 2. CPU密集型的任务,线程数等于CPU数为最大效率(因为频繁的切换线程也要消耗时间)
但有没有更好的配置的建议呢?
答案是有,而且可以更加准确合理的对系统中线程池去进行调配?
1. 核心思路
要想对线程池实现动态管理,第一步就是实现对线程池的状态监控。 你要知道当前线程池负载的运行状况,才能更好的进行调配。
1.1 应该监控线程池什么参数?
我们只关心线程池的关键参数,在ThreadPoolExecutor中有很多参数。 但是对我们更重要的就是以下几点:
- 线程池是不是满负载
- 线程池核心线程数是不是足够(如果不够,那么线程池就会创建新的线程)
- 队列是否充足(反映了任务堆积情况)
- 常驻有效线程(反映了系统的常规运行负载)
所以一些核心的参数还是我们需要关注的,比如:
corePoolSize,queue.size等
1.2 如何获取线程池的监控指标?
要想监控到线程池的状态,首先要将线程池注册到我们的系统中去。 即创建线程池时要显式的进行注册。示例如下:
// 以线程池名称为Key,以线程池对象为Value,进行注册,如果本级用,可以使用ConcurrentHashMap等进行管理
public void register(String threadPoolName, ThreadPoolExecutor executor);
1.3 如何监控?
所谓监控,其实简单理解,就是如何将我们关心的数据,能够定时定量的,把当前的状态反馈给我们。
可以是通过task,ScheduledThreadPoolExecutor, 也可以写脚本运行,甚至通过延时MQ都可以。
2 案例实践
2.1 注册
/**
* 存储了所有的管理线程信息
*/
public static final Map<String, ThreadPoolExecutor> MONITOR_INFO = Maps.newConcurrentMap();
/**
* 监控线程池 核心参数
* @param monitorKey 线程池名称
* @param threadPoolExecutor 具体的线程池参数信息
*/
public void register(String monitorKey, ThreadPoolExecutor threadPoolExecutor) {
try {
Preconditions.checkNotNull(monitorKey, "监控Key不能为空");
Preconditions.checkNotNull(threadPoolExecutor, "监控线程池不能为空");
// 拼接线程池监控指数前缀[这里按照业务,可以指定业务前缀,我这里就采用了直接赋值]
final String fullMonitorKey = monitorKey;
MONITOR_INFO.put(fullMonitorKey, threadPoolExecutor);
} catch (Exception exception) {
LOGGER.error("线程池运行参数监控失败, monitorKey={}", monitorKey, exception);
}
}
2.2 监控
/**
* 开始进行监控计数
* 【注意:这里我是通过一个ScheduledThreadPoolExecutor进行触发的,每隔30s执行一次】
*/
public void startMonitor() {
// 这里通过unmodifiableMap方法,保证统计过程中的数据是不变的
final Map<String, ThreadPoolExecutor> threadPoolExecutorMap = Collections.unmodifiableMap(MONITOR_INFO);
if (threadPoolExecutorMap.isEmpty()) {
return;
}
for (Map.Entry<String, ThreadPoolExecutor> entry : threadPoolExecutorMap.entrySet()) {
THREAD_COUNTER.clear();
String monitorKey = entry.getKey();
final ThreadPoolExecutor threadPoolExecutor = entry.getValue();
if (threadPoolExecutor.isShutdown()) {
// 防止内存泄露
MONITOR_INFO.keySet().removeIf(key -> key.equals(monitorKey));
continue;
}
// 系统默认监控指标【这里也就是我们要统计的核心参数了】
THREAD_COUNTER.put(join(monitorKey, MonitorProperty.CORE_POOL_SIZE), threadPoolExecutor.getCorePoolSize());
THREAD_COUNTER.put(join(monitorKey, MonitorProperty.MAXIMUM_POOL_SIZE), threadPoolExecutor.getMaximumPoolSize());
THREAD_COUNTER.put(join(monitorKey, MonitorProperty.POOL_SIZE), threadPoolExecutor.getPoolSize());
THREAD_COUNTER.put(join(monitorKey, MonitorProperty.TASK_COUNT), threadPoolExecutor.getTaskCount());
THREAD_COUNTER.put(join(monitorKey, MonitorProperty.QUEUE_SIZE), threadPoolExecutor.getQueue().size());
THREAD_COUNTER.put(join(monitorKey, MonitorProperty.ACTIVE_COUNT), threadPoolExecutor.getActiveCount());
THREAD_COUNTER.put(join(monitorKey, MonitorProperty.LARGEST_POOL_SIZE), threadPoolExecutor.getLargestPoolSize());
}
}
// 拼接Key
private String join(String... keys) {
return StringUtils.join(keys, "_"); // commons-lang3.jar中的StringUtils
}
2.3 统计及上报
上一步中,相当于把所有的监控指标可以拿到了。 这里其实就可以根据公司的组件不同来制定不同的统计策略了。 这里举例说明一下:
2.3.1 数据库
上一步中的THREAD_COUNTER其实就是一些核心的数据。我们可以直接按照这种数据格式的维度,存储到常见的数据库中,然后有另外的展示面板进行展示,或者定时导出excel进行离线分析。
2.3.2 实时监控平台
如果你们公司有自己的数据看板,可以直接将这些数据转义公司指定的格式进行上报,就可以实时监测数据看板了
2.3.3 单机写入,离线分析
可以直接将数据写到一个文件服务器的文件中,后续手动进行分析。
3 总结
其实这一步就是通过量化的数据,观测到线程池中的线程状态。从而判断当前的线程池参数配置是否合理。
下一篇会从另一个维度继续和大家探讨对线程池状态的管理,以及一个JDK比较核心的类ManagementFactory
当前,前言中提到的如何做到动态管理,后续也会进行说明。
附:
如果文中有描述失误内容,或者没有描述清楚的,可以将问题发我邮箱,harveytuan@163.com, 如果有其他问题,也可以联系我,大家一起共同讨论。
- 愿大家共同进步,共同成长。