从零到一实现一个简单的动态线程池(1)

42 阅读19分钟

我个人在学习的时候一直主张任何技术都是为了解决痛点而产生的,而近几年动态线程池技术的热切发展也无一例外。

我们首先来谈一下线程池这个概念,只要是稍微具备规模的应用,都会用到这个技术,由于硬件技术的突飞猛进,支持多并发也成为了每个业务系统必不可少的一点。

线程池介绍

线程池是一种基于池化思想管理线程的工具,使用线程池可以减少创建销毁线程的开销,避免线程过多导致系统资源耗尽。充分利用池内计算资源,等待分配并发执行任务,提高系统性能和响应能力

在业务系统开发过程中,线程池有两个常见的应用场景,分别是:快速响应用户请求和快速处理批量任务

线程池应用场景

1. 快速响应用户请求

以电商中的查询商品详情接口举例,从用户发起请求开始,想要获取到商品全部信息,可能会包括获取商品基本信息、库存信息、优惠券以及评论等多个查询逻辑,假设每个查询是 50ms,如果是串行化查询则需要 200ms,查询性能一般。

而如果说通过线程池的方式并行查询,那查询全部商品信息的时间就取决于多个流程中最慢的那一条。

假设优惠信息流程查询时间 80ms,其他流程查询时间 50ms,经过线程池并行优化后,商品详情接口响应时间就是 80ms,通过并行缩短了整体查询时间

import java.util.concurrent.atomic.AtomicInteger;

public class ThreadPoolDemo {

    public static void main(String[] args) {
        // 1. 核心参数定义(模拟 IO 密集型任务配置)
        int corePoolSize = 2;              // 核心线程数
        int maximumPoolSize = 5;           // 最大线程数
        long keepAliveTime = 10;           // 非核心线程空闲存活时间
        TimeUnit unit = TimeUnit.SECONDS;  // 时间单位
        
        // 2. 阻塞队列(有界队列,防止 OOM)
        // 只有当核心线程满了,新任务才会进入队列
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(2);

        // 3. 自定义线程工厂(非常重要,给线程起名,方便日志排查)
        ThreadFactory threadFactory = new NamedThreadFactory("Order-Service");

        // 4. 拒绝策略(当队列满了 且 线程数达到最大值时触发)
        // 这里演示 CallerRunsPolicy:由提交任务的线程自己执行,这是一种通过“降低提交速度”来缓解压力的策略
        RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();

        // 5. 手动创建线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                corePoolSize,
                maximumPoolSize,
                keepAliveTime,
                unit,
                workQueue,
                threadFactory,
                handler
        );

        // 6. 模拟提交任务
        // 场景分析:
        // 任务 1,2 -> 立即由核心线程执行
        // 任务 3,4 -> 核心满了,进入队列
        // 任务 5,6,7 -> 队列满了,创建非核心线程执行(直到达到 max 5)
        // 任务 8+   -> 达到最大线程数且队列已满,触发拒绝策略 (CallerRuns)
        try {
            for (int i = 1; i <= 10; i++) {
                final int taskId = i;
                executor.execute(() -> {
                    try {
                        System.out.println(Thread.currentThread().getName() + " \t正在处理任务: " + taskId);
                        TimeUnit.SECONDS.sleep(1); // 模拟业务耗时
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                });
            }
        } finally {
            // 7. 优雅关闭
            executor.shutdown();
        }
    }

    /**
     * 内部类:自定义线程工厂
     * 作用:为线程设置有意义的名字,生产环境中排查死锁或 CPU 飙高时非常有用。
     */
    static class NamedThreadFactory implements ThreadFactory {
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String namePrefix;

        public NamedThreadFactory(String namePrefix) {
            this.namePrefix = namePrefix + "-Thread-";
        }

        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r, namePrefix + threadNumber.getAndIncrement());
            // 可以在这里设置守护线程、优先级等
            if (t.isDaemon())
                t.setDaemon(false);
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    }
}

线程池固然好用,但它也有属于自己的痛点,而动态线程池是专门为了解决原生JDK的痛点而产生的。 我们先谈谈原生线程池的痛点:

  • 线程池一旦设定即为固定,我们没办法动态的调整它的参数。
  • 线程池随便定义,线程资源过多,造成服务器高负载。
  • 线程池参数不易评估,随着业务的并发提升,业务面临出现故障的风险。
  • 线程池任务堆积,任务执行超时或触发拒绝策略,影响既有业务正常运行。

而动态线程池完美的解决了这一系列痛点,在接着阅读下面的内容的时候,建议大家先看一下美团在2020年提出的动态线程池观念

tech.meituan.com/2020/04/02/…

动态线程池的核心理念可以高度概括为: “将线程池的‘黑盒’运行模式,转变为‘白盒’的可视、可管、可控状态,利用配置中心实现运行时的流量治理。”

1. 配置中心化与动态变更 (Dynamic Configuration)

这是最基础的理念。传统的线程池参数(核心线程数、最大线程数、队列容量)通常写死在代码或静态配置文件中,修改需要重启服务。

  • 核心思想配置即代码,且实时生效
  • 解决痛点:业务流量具有潮汐效应(如双11大促、突发热点),静态配置无法应对动态变化的流量。
  • 实现方式:利用 Nacos、Apollo 等配置中心,监听配置变更,调用 JDK 原生 API(如 setCorePoolSize)或自定义方法(如你修改的 ResizingLinkedBlockingQueue)来实时调整参数。

2. 全链路监控与可视化 (Observability)

原生线程池是一个“黑盒”,开发者通常只知道它在跑,但不知道跑得怎么样。

  • 核心思想看见即掌控
  • 关键指标:不仅要看当前活跃线程数,还要看队列积压情况任务拒绝次数任务平均耗时最大耗时等。
  • 价值:通过监控面板(Grafana 等),开发者可以直观地看到线程池的水位,判断是否需要扩容或优化业务逻辑。

3. 主动预警与告警 (Alerting)

光有监控是不够的,系统需要在问题爆发前通知开发者。

  • 核心思想被动运维转主动治理

  • 场景

    • 当队列积压超过 80% 时,发送告警(防止 OOM)。
    • 当连续触发拒绝策略时,发送告警(防止业务丢单)。
    • 当任务执行时间过长时,发送告警(检测死锁或慢查询)。
  • 你的实现:在代码中通过 DyThreadBeanPostProcessor 注册以及动态代理 RejectedExecutionHandler 来实现告警逻辑,正是这一理念的体现。

4. 优雅的故障隔离与降级 (Isolation & Degradation)

在微服务架构中,不同业务混用线程池会导致雪崩效应。

  • 核心思想资源隔离,互不影响
  • 做法:为核心业务(如订单创建)和非核心业务(如日志打印)分配不同的动态线程池。当非核心业务线程池被打满时,不会拖垮核心业务。
  • 动态性体现:如果某个非核心业务突然占用大量资源,可以通过动态调整将其线程数降到最低,实现“熔断”效果,保护主系统。

总结一个形象的类比:

  • 原生线程池就像是一个固定红绿灯路口。无论车多车少,红灯绿灯的时间是固定的。早高峰车流巨大时,不仅堵死,还可能瘫痪(OOM/拒绝策略),交警(开发者)只能看着干着急,或者把路封了重修(重启服务)。

  • 动态线程池就像是一个智能交通指挥系统

    • 监控(Observability) :摄像头实时统计车流量。
    • 动态调节(Dynamic) :车多了,自动延长绿灯时间(增加核心线程/队列)。
    • 告警(Alerting) :路口彻底堵死时,立即通知指挥中心。
    • 配置中心(Nacos) :指挥中心可以远程一键下发指令,调整所有路口的通行策略。

接下来我们来讲动态线程池的核心也就是动态两个字 :基于配置刷新

什么意思呢, 动态线程池之所以能实现动态,是因为Nacos等配置中心的支持,准确的来说就是当你注册了配置中心的监听器,它会在检查并且拦截配置类的更改,这时候我们就可以基于传入的配置类的信息来修改,这也是实现动态的核心。

我们可以在项目启动的时候动态的更改线程池的配置,而恰好原生线程池有配置的set方法支持我们实现这一步: 举例:

public void setKeepAliveTime(long time, TimeUnit unit) {
    if (time < 0)
        throw new IllegalArgumentException();
    if (time == 0 && allowsCoreThreadTimeOut())
        throw new IllegalArgumentException("Core threads must have nonzero keep alive times");
    long keepAliveTime = unit.toNanos(time);
    long delta = keepAliveTime - this.keepAliveTime;
    this.keepAliveTime = keepAliveTime;
    if (delta < 0)
        interruptIdleWorkers();
}

但还有几个核心痛点需要解决: 首先我们肯定是需要调整阻塞队列的大小的 ,如果使用的是 LinkedBlockingQueue,可能会发现它的容量是固定的,根本不支持动态调整

LinkedBlockingQueue 阻塞队列的容量是 int 类型,如果不设置容量默认是 Integer.MAX_VALUE,设置容量后,因为字段类型是 final,是没有办法变更容量的

image.png

那就需要一个办法来解决这一点了,大家第一时间想到的肯定是基于反射拿到值再进行修改,可以是可以,但有几个局限点

1.高版本开启模块安全检查后,无法修改 final 字段;使用反射需要添加安全机制

2.即使你变小了容量,但容量变小后无法阻塞 原因是因为LinkedBlockingQueue内部的判断机制 具体如下面源码所示 它只会判断是不是与容量相等。假设当前队列已存入 8 个元素,容量为 10。我们通过反射将容量缩小为 5,期望此后入队操作会被阻塞。

image.png

3。LinkedBlockingQueue内部是有阻塞队列的,但你只是简单的更新值,不会唤醒被阻塞的线程

image.png

所以我们只能另辟蹊径:这里参考了 RabbitMQ 团队的设计方案 直接复制并修改LinkedBlockingQueue源码,做成可变容量版本

其逻辑大概如下:

public boolean offer(E o) {
    if (o == null) {
        throw new NullPointerException();
    }
    final AtomicInteger count = this.count;
    if (count.get() >= capacity) {
        return false;
    }
    int c = -1;
    final ReentrantLock putLock = this.putLock;
    putLock.lock();
    try {
        if (count.get() < capacity) {
            insert(o);
            c = count.getAndIncrement();
            if (c + 1 < capacity) {
                notFull.signal();
            }
        }
    } finally {
        putLock.unlock();
    }
    if (c == 0) {
        signalNotEmpty();
    }
    return c >= 0;
}

@Override
public E take() throws InterruptedException {
    E x;
    int c = -1;
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lockInterruptibly();
    try {
        try {
            while (count.get() == 0) {
                notEmpty.await();
            }
        } catch (InterruptedException ie) {
            notEmpty.signal(); // propagate to a non-interrupted thread
            throw ie;
        }

        x = extract();
        c = count.getAndDecrement();
        if (c > 1) {
            notEmpty.signal();
        }
    } finally {
        takeLock.unlock();
    }
    if (c >= capacity) {
        signalNotFull();
    }
    return x;
}

@Override
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
    E x = null;
    int c = -1;
    long nanos = unit.toNanos(timeout);
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lockInterruptibly();
    try {
        for (; ; ) {
            if (count.get() > 0) {
                x = extract();
                c = count.getAndDecrement();
                if (c > 1) {
                    notEmpty.signal();
                }
                break;
            }
            if (nanos <= 0) {
                return null;
            }
            try {
                nanos = notEmpty.awaitNanos(nanos);
            } catch (InterruptedException ie) {
                notEmpty.signal(); // propagate to a non-interrupted thread
                throw ie;
            }
        }
    } finally {
        takeLock.unlock();
    }
    if (c >= capacity) {
        signalNotFull();
    }
    return x;
}

@Override
public E poll() {
    final AtomicInteger count = this.count;
    if (count.get() == 0) {
        return null;
    }
    E x = null;
    int c = -1;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        if (count.get() > 0) {
            x = extract();
            c = count.getAndDecrement();
            if (c > 1) {
                notEmpty.signal();
            }
        }
    } finally {
        takeLock.unlock();
    }
    if (c >= capacity) {
        signalNotFull();
    }
    return x;
}

到这里我们成功完成了容量的热更新

但接下来还有一个问题在等着我们:因为我们需要动态的观测线程池的核心指标,但我们应该如何统计拒绝任务数 原生的四种拒绝策略都不支持我们完成这一点 所以我们需要对它们的功能做一个增强。

尽管线程池的拒绝任务方法被设置为 final 且具有默认访问权限,导致我们无法继承或重写该方法 ,但我们仍可以通过 代理模式 实现扩展功能。

代理模式 (Proxy Design Pattern)是一种在不修改原始类代码的前提下 ,通过引入代理对象对其行为进行增强的设计手段,非常适合用于功能增强、权限控制、延迟加载等场景

常见的代理技术分为静态代理和动态代理

首先来谈谈静态代理 : 静态代理模式需要创建多个具体实现类来增强原始拒绝策略

import java.util.concurrent.atomic.AtomicLong;

/**
 * 动态线程池治理框架 - 拒绝策略增强 Demo
 * 核心亮点:利用接口默认方法与继承机制,实现拒绝策略的可观测性
 */
public class ThreadPoolEnhancementDemo {

    public static void main(String[] args) throws InterruptedException {
        // 1. 初始化自定义线程池
        SupportThreadPoolExecutor executor = new SupportThreadPoolExecutor(
                1,                  // 核心线程数
                1,                  // 最大线程数
                0L, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(1), // 队列容量为 1
                new SupportAbortPolicy()      // 【关键】注入增强后的拒绝策略
        );

        // 2. 模拟高并发场景:提交 5 个任务
        // 预期:Task-0 执行, Task-1 进队列, Task-2/3/4 触发拒绝
        System.out.println("====== 开始提交任务 ======");
        for (int i = 0; i < 5; i++) {
            int taskId = i;
            try {
                executor.execute(() -> {
                    try {
                        // 模拟任务耗时,占住线程不释放
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                });
            } catch (RejectedExecutionException e) {
                // 捕获异常是为了演示程序不中断,实际生产中由业务决定是否捕获
                System.err.println("主线程捕获:任务 " + taskId + " 被拒绝");
            }
        }

        // 3. 验证统计结果
        Thread.sleep(100); // 稍作等待确保计数更新
        System.out.println("\n====== 监控数据 ======");
        System.out.println("线程池历史拒绝次数: " + executor.getRejectCount());
        
        // 关闭线程池
        executor.shutdown();
    }

    // ==========================================
    // 核心组件定义 (Inner Classes)
    // ==========================================

    /**
     * 组件 1: 支持监控能力的自定义线程池
     * 作用:维护业务维度的统计指标(如拒绝次数)
     */
    static class SupportThreadPoolExecutor extends ThreadPoolExecutor {
        private final AtomicLong rejectCount = new AtomicLong(0);

        public SupportThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
            super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), handler);
        }

        // 供拒绝策略回调
        public void incrementRejectCount() {
            rejectCount.incrementAndGet();
        }

        public long getRejectCount() {
            return rejectCount.get();
        }
    }

    /**
     * 组件 2: 增强型拒绝策略接口
     * 作用:利用 default 方法抽离通用的“告警与统计”逻辑
     */
    interface SupportRejectedExecutionHandler extends RejectedExecutionHandler {
        
        /**
         * 拒绝策略前置处理逻辑:统计与告警
         */
        default void beforeReject(ThreadPoolExecutor executor) {
            if (executor instanceof SupportThreadPoolExecutor) {
                SupportThreadPoolExecutor supportExecutor = (SupportThreadPoolExecutor) executor;
                // 1. 拒绝次数自增
                supportExecutor.incrementRejectCount();
                // 2. 模拟发送告警 (Log/DingTalk/Email)
                System.out.println(">>> [告警] 线程池触发了任务拒绝策略!当前拒绝总数:" + supportExecutor.getRejectCount());
            }
        }
    }

    /**
     * 组件 3: 具体的增强策略实现 (以 AbortPolicy 为例)
     * 作用:继承原生策略,并在拒绝前植入切面逻辑
     */
    static class SupportAbortPolicy extends ThreadPoolExecutor.AbortPolicy implements SupportRejectedExecutionHandler {
        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            // AOP 思维:前置增强
            beforeReject(e);
            // 执行原生逻辑 (抛出异常)
            super.rejectedExecution(r, e);
        }
    }
}

但静态代理有它的局限性:

  • 爆炸问题 :每一种拒绝策略都需要手动创建对应的代理类来实现增强逻辑。以 JDK 提供的四种默认策略为例,若都需要扩展,就得创建四个额外类,显然不符合开闭原则 ,也不利于维护。
  • 侵入性较高,增加系统复杂度 :所有线程池都必须显式使用这些增强后的拒绝策略,一旦项目规模扩大或开发人员更替,容易遗漏代理逻辑或出现不一致实现,造成潜在的系统风险。

所以我个人在开发的时候采取了动态代理的模式: 这里使用jdk动态代理劫持接口 例子如下: 这里我们利用 java.lang.reflect.InvocationHandler 来拦截 rejectedExecution 方法。

public class DynamicProxyRejectDemo {

public static void main(String[] args) {
    // 1. 准备一个原始的拒绝策略 (可以是 Abort, CallerRuns 等任意一种)
    RejectedExecutionHandler originalPolicy = new ThreadPoolExecutor.AbortPolicy();

    // 2. 【核心】创建代理对象,把原始策略包进去
    RejectedExecutionHandler proxyPolicy = RejectProxyUtil.createProxy(originalPolicy);

    // 3. 将代理策略放入线程池
    ThreadPoolExecutor executor = new ThreadPoolExecutor(
            1, 1, 0, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(1),
            Executors.defaultThreadFactory(),
            proxyPolicy // <--- 注意这里放的是代理对象
    );

    // 4. 测试触发拒绝
    try {
        // 提交 3 个任务 (1个核心执行, 1个进队列, 第3个触发拒绝)
        for (int i = 0; i < 3; i++) {
            executor.execute(() -> {
                try { Thread.sleep(100); } catch (InterruptedException e) { }
            });
        }
    } catch (RejectedExecutionException e) {
        System.err.println("业务方捕获到拒绝异常: " + e.getMessage());
    }

    executor.shutdown();
}

/**
 * 工具类:专门用于生成代理对象
 */
static class RejectProxyUtil {
    public static RejectedExecutionHandler createProxy(RejectedExecutionHandler target) {
        // 使用 JDK 动态代理生成一个新的 RejectedExecutionHandler 实例
        return (RejectedExecutionHandler) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                new Class[]{RejectedExecutionHandler.class}, // 代理的接口
                new RejectInvocationHandler(target)          // 具体的拦截逻辑
        );
    }
}

/**
 * 核心逻辑:拦截器 (InvocationHandler)
 */
static class RejectInvocationHandler implements InvocationHandler {
    private final RejectedExecutionHandler target;
    private final AtomicLong rejectCount = new AtomicLong(0);

    public RejectInvocationHandler(RejectedExecutionHandler target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 只拦截 rejectedExecution 方法
        if ("rejectedExecution".equals(method.getName())) {
            
            // --- AOP 增强逻辑:前置告警 ---
            long count = rejectCount.incrementAndGet();
            ThreadPoolExecutor executor = (ThreadPoolExecutor) args[1];
            
            System.out.println(String.format(
                "🚨 [动态代理告警] 线程池触发拒绝! 累计次数: %d, 当前策略: %s, 活跃线程: %d",
                count, 
                target.getClass().getSimpleName(),
                executor.getActiveCount()
            ));
            // 可以在这里调用钉钉/飞书发送逻辑
            // -----------------------------
        }

        try {
            // 反射调用原始对象的逻辑 (比如 AbortPolicy 会在这里抛异常)
            return method.invoke(target, args);
        } catch (InvocationTargetException e) {
            // 【关键点】反射会把原始异常包装成 InvocationTargetException
            // 我们必须把里面的原始异常 (如 RejectedExecutionException) 拆包抛出去
            // 否则业务方捕获不到正确的异常类型
            throw e.getCause();
        }
    }
}

}

到目前为止我们解决了动态线程池参数的两大难题。

有没有什么可以高效管理动态线程池方法呢 - 最好的办法是可以依赖于spring的Bean管理机制, 让我们所需要的动态线程池被注册成一个Bean交给spring容器管理,我们可以注册springboot的后置bean处理器, 然后在springboot启动的时候进行动态的检测,是否加上了我们自定义的注解, 如果有自定义的注解,则注册进我们进行动态线程池管理的容器 : 话不多说 - 代码如下:

  1. 我们使用并发安全的Map来当作容器用来保证线程安全 private static final Map<String, ThreadPoolExecutorHolder> HOLDER_MAP = new ConcurrentHashMap<>();

后置Bean处理器: public class DyThreadBeanPostProcessor implements BeanPostProcessor {

private final BootstrapConfigProperties properties;

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
    
    if (bean instanceof DyThreadExecutor) {
        DynamicThreadPool dynamicThreadPool;
        try {
            // 通过 IOC 容器扫描 Bean 是否存在动态线程池注解
            dynamicThreadPool = ApplicationContextHolder.findAnnotationOnBean(beanName, DynamicThreadPool.class);
            if (Objects.isNull(dynamicThreadPool)) {
                return bean;
            }
        } catch (Exception ex) {
            log.error("Failed to create dynamic thread pool in annotation mode.", ex);
            return bean;
        }

       
        DyThreadExecutor dyThreadExecutor = (DyThreadExecutor) bean;
        
        // 从配置中心读取动态线程池配置并对线程池进行赋值
        ThreadPoolExecutorProperties executorProperties = properties.getExecutors()
                .stream()
                .filter(each -> Objects.equals(dyThreadExecutor.getThreadPoolId(), each.getThreadPoolId()))
                .findFirst()
                .orElseThrow(() -> new RuntimeException("The thread pool id does not exist in the configuration."));

        overrideLocalThreadPoolConfig(executorProperties, dyThreadExecutor);
        
        
        DyThreadRegistry.putHolder(dyThreadExecutor.getThreadPoolId(), dyThreadExecutor, executorProperties);
    }

    return bean;
}

}

然后实现自动装配 ,也就是经典三步走类似于把大象装进冰箱:

  1. 写需要被装配的逻辑
  2. 写配置类 把逻辑注册为Bean
  3. 在org.springframework.boot.autoconfigure.AutoConfiguration.imports 这个文件中写自己的配置类的全类名 这样Springboot 启动类在启动的时候可以自动扫描引入的依赖下的文件 把其装配进主应用容器 这也是现在框架常用的 实现了零配置注入 把这个操作交给了框架 组件开发者。

这样我们在主程序中被标记为动态线程池的Bean就会交给核心的Map管理,方便我们进行动态的刷新 实现可追溯 实现Nacos的监听器方法(你也可以使用其它配置中心):

public void registerListener() throws NacosException {
    BootstrapConfigProperties.NacosConfig nacosConfig = properties.getNacos();
    configService.addListener(
            nacosConfig.getDataId(),
            nacosConfig.getGroup(),
            new Listener() {

                @Override
                public Executor getExecutor() {
                    return ThreadPoolExecutorBuilder.builder()
                            .corePoolSize(1)
                            .maximumPoolSize(1)
                            .keepAliveTime(9999L)
                            .workQueueType(BlockingQueueTypeEnum.SYNCHRONOUS_QUEUE)
                            .threadFactory("clod-nacos-refresher-thread_")
                            .rejectedHandler(new ThreadPoolExecutor.CallerRunsPolicy())
                            .build();
                }

                @Override
                public void receiveConfigInfo(String configInfo) {
                    refreshThreadPoolProperties(configInfo);
                }
            });

这段代码就是注册了一个Nacos的监听器,当其检测到配置类发生变更的时候就会执行回调方法,我们可以在方法里执行我们想执行的。

可插拔机制: 大部分框架 组件都提供了可插拔机制 例如FeignClients的 @EnableFeignClients 注解

@SpringBootApplication
@MapperScan("com.nageoffer.shortlink.admin.dao.mapper")
@EnableFeignClients
@EnableDiscoveryClient
public class ShortLinkAdminApplication {

    public static void main(String[] args) {

        System.out.println();
        SpringApplication.run(ShortLinkAdminApplication.class, args);
    }
}

我们可以加入这个注解来启用FeignClients 当我们点击它的源码就会发现 其实他就是一个标记 利用@import注册进了一段逻辑:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({FeignClientsRegistrar.class})
public @interface EnableFeignClients {
    String[] value() default {};

    String[] basePackages() default {};

    Class<?>[] basePackageClasses() default {};

    Class<?>[] defaultConfiguration() default {};

    Class<?>[] clients() default {};
}

也就是FeignClientsRegistrar.class这个类 这个类就是实现FeignClients的核心 如果对其有兴趣可以点进去查看

总的来说 可插拔机制的核心在于“按需加载”,而其实现方式是多种多样的,比如:通过配置文件中的开关(如指定前缀的 Key)、或自定义注解控制模块启用。

由于相关逻辑我们已经在stater文件配置过 所以我们可以不采用FeignClient的直接注入方法 直接使用spring提供的@ConditionalOnProperty 和 @ConditionalOnBean来实现我们的功能 顾名思义 这两个注解的作用是基于指定的配置文件或者类来实现条件注入 , 也就是加在@AutoConfigure下的 下面是代码可以参考一下:


import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

/**
 * 动态线程池自动装配核心入口
 * <p>
 * 设计思想:Marker 模式 + 条件注解,实现“可插拔”的中间件设计
 */
@Configuration
public class DYThreadAutoConfiguration {

    // ==========================================
    // 1. MarkerConfiguration: 开关的“哨兵”
    // ==========================================
    
    /**
     * 只有当配置 DYthread.enable = true (或不配置,默认开启) 时,
     * 这个 Configuration 才会生效,进而创建 Marker Bean。
     */
    @Configuration
    @ConditionalOnProperty(
            prefix = "onethread", 
            name = "enable", 
            havingValue = "true", 
            matchIfMissing = true // 约定大于配置:默认是开启的
    )
    public static class MarkerConfiguration {
        
        @Bean
        public Marker dynamicThreadPoolMarkerBean() {
            return new Marker();
        }

        /**
         * 这是一个没有任何业务逻辑的空类。
         * 它的唯一作用就是作为一个“标记”:
         * 如果 Spring 容器里有这个 Bean,说明用户开启了功能;否则说明用户关闭了。
         */
        public class Marker {
        }
    }

    // ==========================================
    // 2. CommonAutoConfiguration: 真正的业务配置
    // ==========================================

    /**
     * 核心业务配置类
     * 只有当 Marker Bean 存在时,才加载下面的所有 Bean。
     */
    @Configuration
    @ConditionalOnBean(MarkerConfiguration.Marker.class) // 【核心】依赖 Marker 的存在
    @Import(OneThreadBaseConfiguration.class)            // 导入基础配置
    @AutoConfigureAfter(OneThreadBaseConfiguration.class) // 控制加载顺序
    public static class CommonAutoConfiguration {
        
        // 这里定义你的核心 Bean,比如 Monitor, Service 等
        // @Bean
        // public DynamicThreadPoolService dynamicThreadPoolService() { ... }
        
        // 只有当 onethread.enable=true -> MarkerCreated -> CommonAutoConfig 生效
        // 这样的链路保证了系统的稳健性。
    }
}

这样我们就可以实现可插拔机制 到现在为止这个动态线程池终于初具规模 大家可以自己写一个Demo来验证

接下来我们就要实现相关报警逻辑以及可观测体系构建 这些内容放入(2)

由于个人是是刚开始写相关技术博客,大部分内容都是自己手写生成,有些语句可能描述不当,希望可以包容一下。

回到文章开头所说,“任何技术都是为了解决痛点而产生的”。动态线程池技术的出现,正是为了打破原生 JDK 线程池“黑盒运行”和“静态配置”的僵局。

通过本文,我们从零开始构建了一个动态线程池治理框架:利用 Nacos 实现了参数的毫秒级热更新,通过重写阻塞队列打破了容量限制,借助 动态代理 赋予了拒绝策略以监控告警能力,最后利用 Spring Boot Starter 机制实现了“开箱即用”的丝滑体验。

希望通过这个从 0 到 1 的造轮子过程,能让你对 Java 并发编程、反射机制以及 Spring 扩展原理有更深的理解。虽然这是我技术写作的起步,但技术探索的道路永无止境,欢迎大家在评论区交流指正。