借助Nacos配置中心实现一个简易的动态线程池

目录

一、实现思路

二、实现说明概览

三、代码实现

DynamicThreadPool

RejectedProxyInvocationHandler

DynamicThreadPoolRegister

 DynamicThreadPoolRefresher 

测试动态线程池


平常我们系统中定义的一些线程池如果要想修改的话,需要修改配置重启服务才能生效,这样使用起来很不方便,而且对线程池也没有一个监控告警,基于以上原因,我们可以自定义一个可以动态修改线程池配置、对线程池有监控的动态线程池。

美团有一篇技术文章Java线程池实现原理及其在美团业务中的实践,本文的思路也是来源于这篇文章的,有兴趣的同学可以自己看一下。

一、实现思路

我们平常用的线程池ThreadPoolExecutor提供的有修改线程池配置的方法:

  • setCorePoolSize(int corePoolSize) 设置线程池核心线程数量
  • setMaximumPoolSize(int maximumPoolSize) 设置线程池最大线程数量
  • setKeepAliveTime(long time, TimeUnit unit) 设置线程池非核心线程空闲时间
  • setRejectedExecutionHandler(RejectedExecutionHandler handler) 设置线程池拒绝策略

        通过这几个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

二、实现说明概览

借助Nacos配置中心实现一个简易的动态线程池_第1张图片

自定义线程池包含如下四个类:

  • DynamicThreadPool 自定义动态线程池:继承了ThreadPoolExecutor,定义了一个唯一的线程池id
  • RejectedProxyInvocationHandler 线程池拒绝策略代理类:线程池拒绝策略动态代理类,可以在任务被线程池拒绝的时候执行一些自定义的逻辑,例如告警通知
  • DynamicThreadPoolRegister 动态线程池注册器&运行监听器:构建获取DynamicThreadPool实例并将该实例注册到全部Map中,Map中的key和value分别是线程池id和对应的线程池实例,通过Nacos配置变更动态刷新线程池参数配置,另外还有一个后台监听预警任务
  • DynamicThreadPoolRefresher 动态线程池动态刷新处理器:注册Nacos配置监听,调用DynamicThreadPoolRegister的refreshThreadPool方法动态刷新线程池

三、代码实现

DynamicThreadPool

  • 继承了ThreadPoolExecutor,定义了一个唯一的线程池id-threadPoolId
  • 定义了AtomicLong rejectCount用于统计线程池任务拒绝的数量
  • executeTimeOut:通过重写beforeExecute和afterExecute方法,在线程池任务执行前后计算时间差值,即任务的执行时间,可以判断是否大于executeTimeOut任务执行超时时间,超时告警通知
  • RejectedProxyInvocationHandler:线程池拒绝策略动态代理类,可以在任务被线程池拒绝的时候执行一些自定义的逻辑,例如告警通知

        代码如下所示

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

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();
        }
    }
}

DynamicThreadPoolRegister

动态线程池注册器&运行监听器:构建获取DynamicThreadPool实例并将该实例注册到全部Map中,Map中的key和value分别是线程池id和对应的线程池实例,通过Nacos配置变更动态刷新线程池参数配置,另外还有一个后台监听预警任务

  • buildThreadPool:该方法创建一个动态线程池实例,并且将该实例放入threadPoolMap中
  • Map threadPoolMap:Map中的key和value分别是线程池id和对应的线程池实例
  • refreshThreadPool(Map> threadPoolConfig):根据Nacos配置中心的配置动态刷新线程池参数配置,threadPoolConfig参数的key是threadPoolId,value是该线程池的参数配置,例如{"threadPool1":{"coreSize":2,"maxSize":10}}
  • scheduledExecutorService:一个后台定时任务,定时遍历扫描threadPoolMap中的所有线程池实例,获取其运行状态,可以根据活跃的线程数或者队列堆积任务达到一定阈值时进行预警通知
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));
        }
    }
}

 DynamicThreadPoolRefresher 

        动态线程池动态刷新处理器:注册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 set = properties.keySet();
        //解析线程池配置,key是threadPoolId,例如{"threadPool1":{"coreSize":2,"maxSize":10}}
        Map> threadPoolConfig=new HashMap<>();
        for(Object key:set){
            String s = key.toString();
            String[] keyArr = s.split("\\.");
            String threadPoolId=keyArr[2];
            String threadProperty=keyArr[3];
            Integer threadPropertyValue= Integer.valueOf(properties.get(key).toString());
            Map map = threadPoolConfig.getOrDefault(threadPoolId,new HashMap<>());
            map.put(threadProperty,threadPropertyValue);
            threadPoolConfig.put(threadPoolId,map);
            log.info("线程池配置变更了,threadPoolId:{},{}:{}",threadPoolId,threadProperty,threadPropertyValue);
        }
        //TODO 发送钉钉飞书预警
        //根据线程池配置刷新线程池实例
        DynamicThreadPoolRegister.refreshThreadPool(threadPoolConfig);
    }
} 
  

测试动态线程池

        测试类如下所示,构造了一个动态线程池实例,其执行的任务为每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

至此,我们自定义的动态线程池实现完成了。

你可能感兴趣的:(多线程,java,开发语言)