目录
一、实现思路
二、实现说明概览
三、代码实现
DynamicThreadPool
RejectedProxyInvocationHandler
DynamicThreadPoolRegister
DynamicThreadPoolRefresher
测试动态线程池
平常我们系统中定义的一些线程池如果要想修改的话,需要修改配置重启服务才能生效,这样使用起来很不方便,而且对线程池也没有一个监控告警,基于以上原因,我们可以自定义一个可以动态修改线程池配置、对线程池有监控的动态线程池。
美团有一篇技术文章Java线程池实现原理及其在美团业务中的实践,本文的思路也是来源于这篇文章的,有兴趣的同学可以自己看一下。
我们平常用的线程池ThreadPoolExecutor提供的有修改线程池配置的方法:
通过这几个set方法可以在系统运行时动态修改线程池实例的参数
那么如果动态修改配置呢?
可以借助Nacos配置中心来完成,我们将线程池的参数配置在Nacos配置中心,我们修改配置文件时Nacos监听器会监听到线程池配置的变动,我们调用线程池的set方法即可动态修改线程池参数。
如果有多个线程池,怎么知道修改的哪个线程池的配置参数呢?
可以给线程池设置一个id,将线程池注册到一个Map中,Map中的key和value分别是线程池id和对应的线程池实例,我们根据配置中的线程池id去找到对应的线程池实例进行修改。
示例如下:threadPoolId1和threadPoolId2是线程池的唯一标识
dynamic.threadpool.threadPoolId1.coreSize=3
dynamic.threadpool.threadPoolId1.maxSize=8
dynamic.threadpool.threadPoolId2.coreSize=12
dynamic.threadpool.threadPoolId2.maxSize=20
自定义线程池包含如下四个类:
代码如下所示
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.Proxy;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
/**
* @author wkp
* @description 自定义动态线程池
* @create 2023-04-13 10:17
*/
@Getter
@Setter
@Slf4j
public class DynamicThreadPool extends ThreadPoolExecutor {
private String threadPoolId;
private final AtomicLong rejectCount = new AtomicLong();
private Long executeTimeOut;
private final ThreadLocal executeTimeThreadLocal = new ThreadLocal<>();
/**
* 动态线程池构造方法
* @param threadPoolId 动态线程池唯一标识
* @param executeTimeOut 线程任务执行超时时间
* @param corePoolSize 核心线程池数量
* @param maximumPoolSize 最大线程池数量
* @param keepAliveTime 空闲线程活跃时间
* @param unit 时间单位
* @param workQueue 任务队列
* @param threadFactory 线程工厂
* @param redundancyHandler 线程池拒绝策略
*/
public DynamicThreadPool(String threadPoolId,long executeTimeOut,int corePoolSize, int maximumPoolSize,
long keepAliveTime,TimeUnit unit, BlockingQueue workQueue,
ThreadFactory threadFactory,RejectedExecutionHandler redundancyHandler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, redundancyHandler);
this.threadPoolId=threadPoolId;
this.executeTimeOut = executeTimeOut;
//对拒绝策略创建动态代理,在代理里面可以发送预警通知
RejectedExecutionHandler rejectedProxy = createProxy(redundancyHandler, threadPoolId, rejectCount);
setRejectedExecutionHandler(rejectedProxy);
}
public static RejectedExecutionHandler createProxy(RejectedExecutionHandler rejectedExecutionHandler, String threadPoolId, AtomicLong rejectedNum) {
RejectedExecutionHandler rejectedProxy = (RejectedExecutionHandler) Proxy
.newProxyInstance(
rejectedExecutionHandler.getClass().getClassLoader(),
new Class[]{RejectedExecutionHandler.class},
new RejectedProxyInvocationHandler(rejectedExecutionHandler, threadPoolId, rejectedNum));
return rejectedProxy;
}
@Override
public void execute(@NonNull Runnable command) {
super.execute(command);
}
@Override
protected void beforeExecute(Thread t, Runnable r) {
if (executeTimeOut == null) {
return;
}
//线程任务执行之前设置当前时间
executeTimeThreadLocal.set(System.currentTimeMillis());
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
Long startTime=executeTimeThreadLocal.get();
if (startTime == null) {
return;
}
try {
//线程任务执行之后计算时间差值,获取线程任务执行时间是否超时
long endTime = System.currentTimeMillis();
long executeTime;
boolean executeTimeAlarm = (executeTime = (endTime - startTime)) > executeTimeOut;
if (executeTimeAlarm) {
log.info("线程任务执行超时了,threadPoolId:{},executeTime:{}",threadPoolId,executeTime);
//TODO 发送钉钉飞书预警
}
} finally {
executeTimeThreadLocal.remove();
}
}
@Override
public void setCorePoolSize(int corePoolSize){
super.setCorePoolSize(corePoolSize);
}
@Override
public void setMaximumPoolSize(int maximumPoolSize) {
super.setMaximumPoolSize(maximumPoolSize);
}
}
RejectedProxyInvocationHandler 线程池拒绝策略代理类:可以在任务被线程池拒绝的时候执行一些自定义的逻辑,例如拒绝任务数量统计、告警通知等
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.concurrent.atomic.AtomicLong;
/**
* @description 线程池拒绝策略代理类
* @author wkp
* @create 2023/4/13 17:32
*/
@Slf4j
@AllArgsConstructor
public class RejectedProxyInvocationHandler implements InvocationHandler {
private final Object target;
private final String threadPoolId;
private final AtomicLong rejectCount;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//线程池拒绝次数统计
rejectCount.incrementAndGet();
log.info("线程任务执行触发拒绝策略了,threadPoolId:{},rejectCount:{}",threadPoolId,rejectCount.get());
//TODO 发送钉钉飞书预警
try {
return method.invoke(target, args);
} catch (InvocationTargetException ex) {
throw ex.getCause();
}
}
}
动态线程池注册器&运行监听器:构建获取DynamicThreadPool实例并将该实例注册到全部Map中,Map中的key和value分别是线程池id和对应的线程池实例,通过Nacos配置变更动态刷新线程池参数配置,另外还有一个后台监听预警任务
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.MapUtils;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.*;
/**
* @author wkp
* @description 动态线程池注册器&运行监听器
* @create 2023-04-13 10:51
*/
@Slf4j
public class DynamicThreadPoolRegister {
public static final String coreSize = "coreSize";
public static final String maxSize = "maxSize";
private static final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
//存放动态线程池实例,key为threadPoolId
private static Map threadPoolMap = new ConcurrentHashMap<>();
static {
//后台定时任务监控线程池运行状态,可以进行预警
scheduledExecutorService.scheduleAtFixedRate(()->{
Set set = threadPoolMap.keySet();
for(String threadPoolId:set){
DynamicThreadPool pool = threadPoolMap.get(threadPoolId);
//线程池最大线程数
int maximumPoolSize = pool.getMaximumPoolSize();
//线程池当前线程数
int poolSize = pool.getPoolSize();
//活跃线程数
int activeCount = pool.getActiveCount();
BlockingQueue queue = pool.getQueue();
//队列元素个数
int queueSize = queue.size();
// 队列剩余容量
int remainingCapacity = queue.remainingCapacity();
// 队列容量
int queueCapacity = queueSize + remainingCapacity;
//TODO 根据配置的活跃线程数或者队列任务数量阈值进行预警
if(poolSize>maximumPoolSize*0.5){
log.info("线程池负载过高了,poolSize:{},maximumPoolSize{}",threadPoolId,poolSize,maximumPoolSize);
}
}
log.info("线程池监控定时任务,threadPoolSize:{}",threadPoolMap.size());
},5,10,TimeUnit.SECONDS);
}
public static ThreadPoolExecutor buildThreadPool(String threadPoolId, long executeTimeOut, int corePoolSize, int maximumPoolSize) {
return buildThreadPool(threadPoolId, executeTimeOut, corePoolSize, maximumPoolSize, 60,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(10), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());
}
public static ThreadPoolExecutor buildThreadPool(String threadPoolId, long executeTimeOut, int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit, BlockingQueue workQueue,
ThreadFactory threadFactory, RejectedExecutionHandler redundancyHandler) {
DynamicThreadPool dynamicThreadPool = new DynamicThreadPool(threadPoolId, executeTimeOut, corePoolSize,
maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, redundancyHandler);
threadPoolMap.put(threadPoolId, dynamicThreadPool);
return dynamicThreadPool;
}
/**
* 刷新线程池配置
* @param threadPoolConfig
*/
public static void refreshThreadPool(Map> threadPoolConfig) {
if (MapUtils.isEmpty(threadPoolConfig)) {
return;
}
Set set = threadPoolConfig.keySet();
for (String threadPoolId : set) {
DynamicThreadPool dynamicThreadPool = threadPoolMap.get(threadPoolId);
if (Objects.isNull(dynamicThreadPool)) {
continue;
}
Map propertyMap = threadPoolConfig.get(threadPoolId);
//根据配置修改线程池的线程数,队列,拒绝策略等等
dynamicThreadPool.setCorePoolSize(propertyMap.get(coreSize));
dynamicThreadPool.setMaximumPoolSize(propertyMap.get(maxSize));
}
}
}
动态线程池动态刷新处理器:注册Nacos配置监听,调用DynamicThreadPoolRegister的refreshThreadPool方法动态刷新线程池
前提是项目中要引入Nacos配置中心,Nacos的pom依赖我就不写了,项目中的Nacos配置如下
#nacos注册中心、配置中心地址
spring.cloud.nacos.discovery.server-addr=nacos-host:80
spring.cloud.nacos.config.file-extension=properties
spring.cloud.nacos.config.group=DEFAULT_GROUP
spring.cloud.nacos.config.server-addr=nacos-host:80
#动态线程池配置文件
spring.cloud.nacos.config.extension-configs[0].data-id=dynamic-threadpool.properties
spring.cloud.nacos.config.extension-configs[0].group=DEFAULT_GROUP
spring.cloud.nacos.config.extension-configs[0].refresh=true
nacos配置文件dynamic-threadpool.properties如下所示:
dynamic.threadpool.threadPoolId1.coreSize=3
dynamic.threadpool.threadPoolId1.maxSize=8
DynamicThreadPoolRefresher代码如下:
import com.alibaba.cloud.nacos.NacosConfigManager;
import com.alibaba.nacos.api.config.listener.Listener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.io.IOException;
import java.io.StringReader;
import java.util.*;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
/**
* @author wkp
* @description 动态线程池动态刷新处理器
* @create 2023-04-13 11:04
*/
@Slf4j
@Component
public class DynamicThreadPoolRefresher implements InitializingBean {
@Resource
private NacosConfigManager nacosConfigManager;
@Override
public void afterPropertiesSet() throws Exception {
//对nacos中动态线程池的配置文件dynamic-threadpool.properties注册监听
nacosConfigManager.getConfigService().addListener("dynamic-threadpool.properties", "DEFAULT_GROUP",
new Listener() {
@Override
public Executor getExecutor() {
return Executors.newSingleThreadExecutor();
}
@Override
public void receiveConfigInfo(String configInfo) {
dynamicRefresh(configInfo);
}
});
}
private void dynamicRefresh(String configInfo) {
log.info(configInfo);
Properties properties = new Properties();
try {
properties.load(new StringReader(configInfo));
} catch (IOException e) {
e.printStackTrace();
}
//配置示例
//dynamic.threadpool.threadPoolId1.coreSize=5
//dynamic.threadpool.threadPoolId1.maxSize=10
//dynamic.threadpool.threadPoolId2.coreSize=12
//dynamic.threadpool.threadPoolId2.maxSize=20
Set
测试类如下所示,构造了一个动态线程池实例,其执行的任务为每5秒打印自己的核心线程数和最大线程数。
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;
import java.util.concurrent.ThreadPoolExecutor;
/**
* @author wkp
* @description 测试动态线程池
* @create 2023-04-13 16:47
*/
@Component
@Slf4j
public class TestDynamicThreadPool implements InitializingBean {
private ThreadPoolExecutor threadPoolExecutor;
@Override
public void afterPropertiesSet() throws Exception {
threadPoolExecutor=DynamicThreadPoolRegister.buildThreadPool("threadPoolId1",1000,2,5);
threadPoolExecutor.execute(()->{
while(true) {
try {
Thread.sleep(5000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("threadPoolId1执行后台任务coreSize:{},maxSize:{}", threadPoolExecutor.getCorePoolSize(), threadPoolExecutor.getMaximumPoolSize());
}
});
}
}
启动服务,观察控制台输出的日志:定时监听器每10秒执行一次监听,自定义的任务每5秒输出一次线程池的线程参数
2023-04-13 19:25:14.451 +0800 [TID: N/A] [pool-9-thread-1] INFO c.b.t.u.i.t.TestDynamicThreadPool:31 - threadPoolId1执行后台任务coreSize:2,maxSize:5
2023-04-13 19:25:19.447 +0800 [TID: N/A] [pool-8-thread-1] INFO c.b.t.u.i.t.DynamicThreadPoolRegister:48 - 线程池监控定时任务,threadPoolSize:1
2023-04-13 19:25:19.452 +0800 [TID: N/A] [pool-9-thread-1] INFO c.b.t.u.i.t.TestDynamicThreadPool:31 - threadPoolId1执行后台任务coreSize:2,maxSize:5
2023-04-13 19:25:24.453 +0800 [TID: N/A] [pool-9-thread-1] INFO c.b.t.u.i.t.TestDynamicThreadPool:31 - threadPoolId1执行后台任务coreSize:2,maxSize:5
2023-04-13 19:25:29.448 +0800 [TID: N/A] [pool-8-thread-1] INFO c.b.t.u.i.t.DynamicThreadPoolRegister:48 - 线程池监控定时任务,threadPoolSize:1
修改Nacos中的线程池配置后,再观察控制台日志:可以看到线程池的参数已经发生了变化。
2023-04-13 19:25:48.135 +0800 [TID: N/A] [pool-14-thread-1] INFO c.b.t.u.i.t.DynamicThreadPoolRefresher:71 - 线程池配置变更了,threadPoolId:threadPoolId1,maxSize:9
2023-04-13 19:25:48.878 +0800 [TID: N/A] [pool-14-thread-1] INFO c.b.t.u.i.t.DynamicThreadPoolRefresher:71 - 线程池配置变更了,threadPoolId:threadPoolId2,maxSize:20
2023-04-13 19:25:53.136 +0800 [TID: N/A] [pool-9-thread-1] INFO c.b.t.u.i.t.TestDynamicThreadPool:31 - threadPoolId1执行后台任务coreSize:4,maxSize:9
至此,我们自定义的动态线程池实现完成了。