一时技痒,撸了个动态线程池,源码放Github了
阐述背景
线程池在日常工作中用的还挺多,当需要异步,批量处理一些任务的时候我们会定义一个线程池来处理。
在使用线程池的过程中有一些问题,下面简单介绍下之前遇到的一些问题。
场景一:实现一些批量处理数据的功能,刚开始线程池的核心线程数设的比较小,然后想调整下,只能改完后重启应用。
场景二:有一个任务处理的应用,会接收 MQ 的消息进行任务的处理,线程池的队列也允许缓存一定数量的任务。当任务处理的很慢的时候,想看看到底有多少没有处理完不是很方便。当时为了快速方便,就直接启动了一个线程去循环打印线程池队列的大小。
正好之前在我公众号有转发过美团的一篇线程池应用的文章(https://mp.weixin.qq.com/s/tIWAocevZThfbrfWoJGa9w),觉得他们的思路非常好,就是没有开放源码,所以自己就抽时间在我的开源项目 Kitty 中增加了一个动态线程池的组件,支持了 Cat 监控,动态变更核心参数,任务堆积告警等。今天就给大家分享一下实现的方式。
项目源代码地址:https://github.com/yinjihuan/kitty
使用方式
添加依赖
依赖线程池的组件,目前 Kitty 未发布,需要自己下载源码 install 本地或者私有仓库。
<dependency>
<groupId>com.cxytiandi</groupId>
<artifactId>kitty-spring-cloud-starter-dynamic-thread-pool</artifactId>
</dependency>
添加配置
然后在 Nacos 配置线程池的信息,我的这个整合了 Nacos。推荐一个应用创建一个单独的线程池配置文件,比如我们这个叫 dataId 为 kitty-cloud-thread-pool.properties,group 为 BIZ_GROUP。
内容如下:
kitty.threadpools.nacosDataId=kitty-cloud-thread-pool.properties
kitty.threadpools.nacosGroup=BIZ_GROUP
kitty.threadpools.accessToken=ae6eb1e9e6964d686d2f2e8127d0ce5b31097ba23deee6e4f833bc0a77d5b71d
kitty.threadpools.secret=SEC6ec6e31d1aa1bdb2f7fd5eb5934504ce09b65f6bdc398d00ba73a9857372de00
kitty.threadpools.owner=尹吉欢
kitty.threadpools.executors[0].threadPoolName=TestThreadPoolExecutor
kitty.threadpools.executors[0].corePoolSize=4
kitty.threadpools.executors[0].maximumPoolSize=4
kitty.threadpools.executors[0].queueCapacity=5
kitty.threadpools.executors[0].queueCapacityThreshold=5
kitty.threadpools.executors[1].threadPoolName=TestThreadPoolExecutor2
kitty.threadpools.executors[1].corePoolSize=2
kitty.threadpools.executors[1].maximumPoolSize=4
nacosDataId,nacosGroup
监听配置修改的时候需要知道监听哪个 DataId,值就是当前配置的 DataId。
accessToken,secret
钉钉机器人的验证信息,用于告警。
owner
这个应用的负责人,告警的消息中会显示。
threadPoolName
线程池的名称,使用的时候需要关注。
剩下的配置就不一一介绍了,跟线程池内部的参数一致,还有一些可以查看源码得知。
注入使用
@Autowired
private DynamicThreadPoolManager dynamicThreadPoolManager;
dynamicThreadPoolManager.getThreadPoolExecutor("TestThreadPoolExecutor").execute(() -> {
log.info("线程池的使用");
try {
Thread.sleep(30000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "getArticle");
通过 DynamicThreadPoolManager 的 getThreadPoolExecutor 方法获取线程池对象,然后传入 Runnable,Callable 等。第二个参数是这个任务的名称,之所以要扩展一个参数是因为如果任务没有标识,那么无法区分任务。
这个线程池组件默认集成了 Cat 打点,设置了名称可以在 Cat 上查看这个任务相关的监控数据。
扩展功能
任务执行情况监控
在 Cat 的 Transaction 报表中会以线程池的名称为类型显示。
详情中会以任务的名称显示。
核心参数动态修改
核心参数目前只支持 corePoolSize,maximumPoolSize,queueCapacity(队列类型为 LinkedBlockingDeque 才可以修改),rejectedExecutionType,keepAliveTime,unit 这些参数的修改。
一般 corePoolSize,maximumPoolSize,queueCapacity 是最常要动态改变的。
需要改动的话直接在 Nacos 中将对应的配置值修改即可,客户端会监听配置的修改,然后同步修改先线程池的参数。
队列容量告警
queueCapacityThreshold 是队列容量告警的阀值,如果队列中的任务数量超过了 queueCapacityThreshold 就会告警。
拒绝次数告警
当队列容量满了后,新进来的任务会根据用户设置的拒绝策略去选择对应的处理方式。如果是采用 AbortPolicy 策略,也会进行告警。相当于消费者已经超负荷了。
线程池运行情况
底层对接了 Cat,所以将线程的运行数据上报给了 Cat。我们可以在 Cat 中查看这些信息。
如果你想在自己的平台去展示,我这边暴露了/actuator/thread-pool 端点,你可以自行拉取数据。
{
threadPools: [{
threadPoolName: "TestThreadPoolExecutor",
activeCount: 0,
keepAliveTime: 0,
largestPoolSize: 4,
fair: false,
queueCapacity: 5,
queueCapacityThreshold: 2,
rejectCount: 0,
waitTaskCount: 0,
taskCount: 5,
unit: "MILLISECONDS",
rejectedExecutionType: "AbortPolicy",
corePoolSize: 4,
queueType: "LinkedBlockingQueue",
completedTaskCount: 5,
maximumPoolSize: 4
}, {
threadPoolName: "TestThreadPoolExecutor2",
activeCount: 0,
keepAliveTime: 0,
largestPoolSize: 0,
fair: false,
queueCapacity: 2147483647,
queueCapacityThreshold: 2147483647,
rejectCount: 0,
waitTaskCount: 0,
taskCount: 0,
unit: "MILLISECONDS",
rejectedExecutionType: "AbortPolicy",
corePoolSize: 2,
queueType: "LinkedBlockingQueue",
completedTaskCount: 0,
maximumPoolSize: 4
}]
}
自定义拒绝策略
平时我们使用代码创建线程池可以自定义拒绝策略,在构造线程池对象的时候传入即可。这里由于创建线程池都被封装好了,我们只能在 Nacos 配置拒绝策略的名称来使用对应的策略。默认是可以配置 JDK 自带的 CallerRunsPolicy,AbortPolicy,DiscardPolicy,DiscardOldestPolicy 这四种。
如果你想自定义的话也是支持的,定义方式跟以前一样,如下:
@Slf4j
public class MyRejectedExecutionHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
log.info("进来了。。。。。。。。。");
}
}
要让这个策略生效的话使用的是 SPI 的方式,需要在 resources 下面创建一个 META-INF 的文件夹,然后创建一个 services 的文件夹,再创建一个 java.util.concurrent.RejectedExecutionHandler 的文件,内容为你定义的类全路径。
自定义告警方式
默认是内部集成了钉钉机器人的告警方式,如果你不想用也可以将其关闭。或者将告警信息对接到你的监控平台去。
如果没有告警平台也可以在项目中实现新的告警方式,比如短信等。
只需要实现 ThreadPoolAlarmNotify 这个类即可。
/**
* 自定义短信告警通知
*
* @作者 尹吉欢
* @个人微信 jihuan900
* @微信公众号 猿天地
* @GitHub https://github.com/yinjihuan
* @作者介绍 http://cxytiandi.com/about
* @时间 2020-05-27 22:26
*/
@Slf4j
@Component
public class ThreadPoolSmsAlarmNotify implements ThreadPoolAlarmNotify {
@Override
public void alarmNotify(AlarmMessage alarmMessage) {
log.info(alarmMessage.toString());
}
}
代码实现
具体的就不讲的很细了,源码在https://github.com/yinjihuan/kitty/tree/master/kitty-dynamic-thread-pool,大家自己去看,并不复杂。
创建线程池
根据配置创建线程池,ThreadPoolExecutor 是自定义的,因为需要做 Cat 埋点。
/**
* 创建线程池
* @param threadPoolProperties
*/
private void createThreadPoolExecutor(DynamicThreadPoolProperties threadPoolProperties) {
threadPoolProperties.getExecutors().forEach(executor -> {
KittyThreadPoolExecutor threadPoolExecutor = new KittyThreadPoolExecutor(
executor.getCorePoolSize(),
executor.getMaximumPoolSize(),
executor.getKeepAliveTime(),
executor.getUnit(),
getBlockingQueue(executor.getQueueType(), executor.getQueueCapacity(), executor.isFair()),
new KittyThreadFactory(executor.getThreadPoolName()),
getRejectedExecutionHandler(executor.getRejectedExecutionType(), executor.getThreadPoolName()), executor.getThreadPoolName());
threadPoolExecutorMap.put(executor.getThreadPoolName(), threadPoolExecutor);
});
}
刷新线程池
首先需要监听 Nacos 的修改。
/**
* 监听配置修改,spring-cloud-alibaba 2.1.0版本不支持@NacosConfigListener的监听
*/
public void initConfigUpdateListener(DynamicThreadPoolProperties dynamicThreadPoolProperties) {
ConfigService configService = nacosConfigProperties.configServiceInstance();
try {
configService.addListener(dynamicThreadPoolProperties.getNacosDataId(), dynamicThreadPoolProperties.getNacosGroup(), new AbstractListener() {
@Override
public void receiveConfigInfo(String configInfo) {
new Thread(() -> refreshThreadPoolExecutor()).start();
log.info("线程池配置有变化,刷新完成");
}
});
} catch (NacosException e) {
log.error("Nacos配置监听异常", e);
}
}
然后再刷新线程池的参数信息,由于监听事件触发的时候,这个时候配置其实还没刷新,所以我就等待了 1 秒钟,让配置完成刷新然后直接从配置类取值。
虽然有点挫还是可以用,其实更好的方式是解析 receiveConfigInfo 那个 configInfo,configInfo 就是改变之后的整个配置内容。因为不太好解析成属性文件,就没做,后面再改吧。
/**
* 刷新线程池
*/
private void refreshThreadPoolExecutor() {
try {
// 等待配置刷新完成
Thread.sleep(1000);
} catch (InterruptedException e) {
}
dynamicThreadPoolProperties.getExecutors().forEach(executor -> {
ThreadPoolExecutor threadPoolExecutor = threadPoolExecutorMap.get(executor.getThreadPoolName());
threadPoolExecutor.setCorePoolSize(executor.getCorePoolSize());
threadPoolExecutor.setMaximumPoolSize(executor.getMaximumPoolSize());
threadPoolExecutor.setKeepAliveTime(executor.getKeepAliveTime(), executor.getUnit());
threadPoolExecutor.setRejectedExecutionHandler(getRejectedExecutionHandler(executor.getRejectedExecutionType(), executor.getThreadPoolName()));
BlockingQueue<Runnable> queue = threadPoolExecutor.getQueue();
if (queue instanceof ResizableCapacityLinkedBlockIngQueue) {
((ResizableCapacityLinkedBlockIngQueue<Runnable>) queue).setCapacity(executor.getQueueCapacity());
}
});
}
其他的刷新都是线程池自带的,需要注意的是线程池队列大小的刷新,目前只支持 LinkedBlockingQueue 队列,由于 LinkedBlockingQueue 的大小是不允许修改的,所以按照美团那篇文章提供的思路,自定义了一个可以修改的队列,其实就是把 LinkedBlockingQueue 的代码复制了一份,改一下就可以。
往 Cat 上报运行信息
往 Cat 的 Heartbeat 报表上传数据的代码如下,主要还是 Cat 本身提供了扩展的能力。只需要定时去调用下面的方式上报数据即可。
public void registerStatusExtension(ThreadPoolProperties prop, KittyThreadPoolExecutor executor) {
StatusExtensionRegister.getInstance().register(new StatusExtension() {
@Override
public String getId() {
return "thread.pool.info." + prop.getThreadPoolName();
}
@Override
public String getDescription() {
return "线程池监控";
}
@Override
public Map<String, String> getProperties() {
AtomicLong rejectCount = getRejectCount(prop.getThreadPoolName());
Map<String, String> pool = new HashMap<>();
pool.put("activeCount", String.valueOf(executor.getActiveCount()));
pool.put("completedTaskCount", String.valueOf(executor.getCompletedTaskCount()));
pool.put("largestPoolSize", String.valueOf(executor.getLargestPoolSize()));
pool.put("taskCount", String.valueOf(executor.getTaskCount()));
pool.put("rejectCount", String.valueOf(rejectCount == null ? 0 : rejectCount.get()));
pool.put("waitTaskCount", String.valueOf(executor.getQueue().size()));
return pool;
}
});
}
定义线程池端点
通过自定义端点来暴露线程池的配置和运行的情况,可以让外部的监控系统拉取数据做对应的处理。
@Endpoint(id = "thread-pool")
public class ThreadPoolEndpoint {
@Autowired
private DynamicThreadPoolManager dynamicThreadPoolManager;
@Autowired
private DynamicThreadPoolProperties dynamicThreadPoolProperties;
@ReadOperation
public Map<String, Object> threadPools() {
Map<String, Object> data = new HashMap<>();
List<Map> threadPools = new ArrayList<>();
dynamicThreadPoolProperties.getExecutors().forEach(prop -> {
KittyThreadPoolExecutor executor = dynamicThreadPoolManager.getThreadPoolExecutor(prop.getThreadPoolName());
AtomicLong rejectCount = dynamicThreadPoolManager.getRejectCount(prop.getThreadPoolName());
Map<String, Object> pool = new HashMap<>();
Map config = JSONObject.parseObject(JSONObject.toJSONString(prop), Map.class);
pool.putAll(config);
pool.put("activeCount", executor.getActiveCount());
pool.put("completedTaskCount", executor.getCompletedTaskCount());
pool.put("largestPoolSize", executor.getLargestPoolSize());
pool.put("taskCount", executor.getTaskCount());
pool.put("rejectCount", rejectCount == null ? 0 : rejectCount.get());
pool.put("waitTaskCount", executor.getQueue().size());
threadPools.add(pool);
});
data.put("threadPools", threadPools);
return data;
}
}
Cat 监控线程池中线程的执行时间
本来是将监控放在 KittyThreadPoolExecutor 的 execute,submit 方法里的。后面测试下来发现有问题,数据在 Cat 上确实有了,但是执行时间都是 1 毫秒,也就是没生效。
不说想必大家也知道,因为线程是后面单独去执行的,所以再添加任务的地方埋点没任务意义。
后面还是想到了一个办法来实现埋点的功能,就是利用线程池提供的 beforeExecute 和 afterExecute 两个方法,在线程执行之前和执行之后都会触发这两个方法。
@Override
protected void beforeExecute(Thread t, Runnable r) {
String threadName = Thread.currentThread().getName();
Transaction transaction = Cat.newTransaction(threadPoolName, runnableNameMap.get(r.getClass().getSimpleName()));
transactionMap.put(threadName, transaction);
super.beforeExecute(t, r);
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
String threadName = Thread.currentThread().getName();
Transaction transaction = transactionMap.get(threadName);
transaction.setStatus(Message.SUCCESS);
if (t != null) {
Cat.logError(t);
transaction.setStatus(t);
}
transaction.complete();
transactionMap.remove(threadName);
}
后面的代码大家自己去看就行了,本文到这里就结束了。如果感觉本文还不错的记得转发下哦!
多谢多谢。
最后感谢美团技术团队的那篇文章,虽然没有分享源码,但是思路什么的和应用场景都讲的很明白。
感兴趣的 Star 下呗:https://github.com/yinjihuan/kitty
一时技痒,撸了个动态线程池,源码放Github了的更多相关文章
- 动态线程池(DynamicTp)之动态调整Tomcat、Jetty、Undertow线程池参数篇
大家好,这篇文章我们来介绍下动态线程池框架(DynamicTp)的adapter模块,上篇文章也大概介绍过了,该模块主要是用来适配一些第三方组件的线程池管理,让第三方组件内置的线程池也能享受到动态参数 ...
- 美团动态线程池实践思路开源项目(DynamicTp),线程池源码解析及通知告警篇
大家好,这篇文章我们来聊下动态线程池开源项目(DynamicTp)的通知告警模块.目前项目提供以下通知告警功能,每一个通知项都可以独立配置是否开启.告警阈值.告警间隔时间.平台等,具体代码请看core ...
- 动态线程池框架 DynamicTp v1.0.6版本发布。还在为Dubbo线程池耗尽烦恼吗?还在为Mq消费积压烦恼吗?
DynamicTp 简介 DynamicTp 是一个基于配置中心实现的轻量级动态线程池管理工具,主要功能可以总结为 动态调参.通知报警.运行监控.三方包线程池管理等几大类. 经过几个版本迭代,目前最新 ...
- 历时2月,动态线程池 DynamicTp 发布里程碑版本 V1.0.8
关于 DynamicTp DynamicTp 是一个基于配置中心实现的轻量级动态线程池管理工具,主要功能可以总结为动态调参.通知报警.运行监控.三方包线程池管理等几大类. 经过多个版本迭代,目前最新版 ...
- 我的开源之路:耗时 6 个月发布线程池框架,GitHub 1.7k Star!
文章首发在公众号(龙台的技术笔记),之后同步到掘金和个人网站:xiaomage.info Hippo4J 线程池框架经过 6 个多月的版本迭代,2022 年春节当天成功发行了 1.0.0 RELEAS ...
- java多线程——线程池源码分析(一)
本文首发于cdream的个人博客,点击获得更好的阅读体验! 欢迎转载,转载请注明出处. 通常应用多线程技术时,我们并不会直接创建一个线程,因为系统启动一个新线程的成本是比较高的,涉及与操作系统的交互, ...
- java多线程----线程池源码分析
http://www.cnblogs.com/skywang12345/p/3509954.html 线程池示例 在分析线程池之前,先看一个简单的线程池示例. 1 import java.util.c ...
- spring动态线程池(实质还是用了java的线程池)
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.sp ...
- 线程池之ThreadPoolExecutor线程池源码分析笔记
1.线程池的作用 一方面当执行大量异步任务时候线程池能够提供较好的性能,在不使用线程池的时候,每当需要执行异步任务时候是直接 new 一线程进行运行,而线程的创建和销毁是需要开销的.使用线程池时候,线 ...
随机推荐
- 自定义realm实现授权
对用户和权限封装 更具用户查询已经拥有的角色 实现类 根据用户查询已经拥有的权限
- jmeter之cookies管理器的使用
作用: 1 发送请求,经常要校验cookies信息 2 针对有的cookie是用的sessionid来进行校验的,这个就需要自己去配置cookie管理器里面的信息,而且sessionid是有时效性的 ...
- Linux操作系统分析 | 深入理解系统调用
实验要求 1.找一个系统调用,系统调用号为学号最后2位相同的系统调用 2.通过汇编指令触发该系统调用 3.通过gdb跟踪该系统调用的内核处理过程 4.重点阅读分析系统调用入口的保存现场.恢复现场和系统 ...
- [JavaWeb基础] 004.用JSP + SERVLET 进行简单的增加删除修改
上一次的文章,我们讲解了如何用JAVA访问MySql数据库,对数据进行增加删除修改查询.那么这次我们把具体的页面的数据库操作结合在一起,进行一次简单的学生信息操作案例. 首先我们创建一个专门用于学生管 ...
- volatile关键字与内存可见性&原子变量与CAS算法
1 .volatile 关键字:当多个线程进行操作共享数据时, 可以保证内存中的数据可见 2 .原子变量:jdk1.5后java.util.concurrent.atomic 包下提供常用的原子变量 ...
- 编译sifive的freedom-u-sdk
在其它电脑搭建编译该sdk工程的环境,由于所在电脑的linux系统为新装系统(版本:Ubuntu 20.04 LTS),下面记录了编译过程中遇到的问题,以及解决过程供以后参考 问题1:error &q ...
- 关于Dev-C++一种引用头文件<iostream>问题(暴力解决)
问题情况如下,因个人水平有限,不知道具体原因是啥,当引用头文件<iostream>时会出现如下问题,经排查,并不是头文件本身的问题,有可能是Dev哪一个文件被改动了,或者设置出了问题(前者 ...
- meavn项目由打包方式jar改为war报Cannot install Dynamic Web Module 2.5 facet. It is incompatibile with already installed facets: Utility Module. Please modify project configuration.处理方式
找到 \项目名\.setting\文件夹下的 org.eclipse.wst.common.project.facet.core.xml xml文件. 添加或修改 <installed ...
- 关于Vue data对象赋值的问题
遇到这么一个问题: 把data中的某个对象赋值给一个变量,修改变量,会同时把data中的对象也一同修改,所以,这个赋值应该就是引用了地址,贴个代码 <script> export defa ...
- Matlab矩阵学习一 矩阵的创建
Matlab矩阵创建 1.直接输入数值创建 矩阵元素要用[ ] 括起来,";"代表一行结束,以下创建方式也是合法的,矩阵的元素可以是实数,也可以是复数,复数用a+bi表 ...