统一管理项目线程池

本文探讨了频繁创建和销毁线程带来的问题,提出通过Spring管理的统一线程池来解决,包括自定义线程池配置、使用shutdownHook进行优雅停机,以及如何在Spring上下文中捕获和处理线程异常。
摘要由CSDN通过智能技术生成

一、问题描述

频繁的创建和销毁线程以及线程池,无疑会给系统带来沉重的负担。这种操作不仅会导致系统资源的浪费,还会因为线程管理的复杂性而增加额外的开销。更为严重的是,如果线程没有得到有效的池化和统一管理,系统内线程数的上限将变得不可控。一旦线程数过多,就会占用大量的系统资源,可能导致系统性能下降,甚至引发崩溃。
以下代码为例,每次执行方法时都会创建一个新的线程池。然而,当业务逻辑执行完毕后,这些线程池并没有被正确销毁或回收。这种做法不仅造成了资源的浪费,还可能因为线程池的不断累积,导致系统内线程数迅速增加,进而引发一系列的性能问题。

        ExecutorService executorService = Executors.newSingleThreadExecutor();
        CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {

            return "hello world";
        }, executorService);

在频繁创建和销毁线程以及线程池的情况下,随着系统访问量的增加,未得到有效管理的线程数会持续增长,从而加剧CPU的负载。当线程数增长到一定程度时,CPU资源可能会被完全占用,导致系统性能急剧下降,最终可能使整个服务变得不可用。
为了解决上述问题,避免系统资源的过度消耗和服务的崩溃,我们需要采取更有效的线程管理方式。一个可行的方案是引入统一的线程池配置,以替代之前自建的线程和线程池。

二、自建线程池

ThreadPoolConfig中,创建我们项目统一的线程池,并交给spring管理。

@Configuration
@EnableAsync
public class ThreadPoolConfig implements AsyncConfigurer {
    /**
     * 项目共用线程池
     */
    public static final String COMMON_EXECUTOR = "commonExecutor";
    /**
     * 邮箱线程池
     */
    public static final String MAIL_EXECUTOR = "mailExecutor";

    @Override
    public Executor getAsyncExecutor() {
        return commonExecutor();
    }

    @Bean(COMMON_EXECUTOR)
    @Primary
    public ThreadPoolTaskExecutor commonExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(200);
        executor.setThreadNamePrefix("test-executor-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());//满了调用线程执行,认为重要任务
        executor.initialize();
        return executor;
    }
}

在这个过程中,我们完成了两项重要的任务。首先,我们创建了一个统一的线程池,它的目的是将系统中所有的线程管理集中起来,确保资源的有效利用和系统的稳定运行。其次,我们通过实现AsyncConfigurer接口,进一步配置了@async注解,使得这些异步操作能够使用我们创建的统一线程池。这样的设计使得线程的管理更加便捷和统一,有利于我们后续对线程进行监控和优化。

值得一提的是,我们在创建线程池时并没有采用Executors提供的快速创建方法。这是因为Executors创建的线程池默认使用无界队列,虽然在一定程度上简化了线程池的创建过程,但却存在潜在的风险。当任务提交过于频繁,线程池中的线程无法及时处理时,无界队列会不断地堆积任务,最终可能导致内存溢出(OOM)的风险。

为了解决这个问题,我们选择了自定义线程池的配置。在创建线程池时,我们设置了合理的线程数量、队列大小等参数,确保系统在高并发场景下也能稳定运行。此外,我们还通过executor.setThreadNamePrefix(“test-executor-”)设置了线程的前缀。这样做的好处在于,当我们在排查CPU占用过高、死锁问题或其他bug时,可以根据线程名快速定位问题所在,判断是业务逻辑的问题还是底层框架的问题,从而大大提高问题排查的效率。

三、优雅停机

在项目结束之际,为了确保所有待处理任务能够顺利完成,防止任务丢失,我们需要通过JVM的shutdownHook机制回调线程池。这一步骤的核心在于,在JVM关闭之前,触发一个回调操作,通知线程池开始执行其关闭流程。线程池在接收到这一信号后,会开始处理队列中剩余的任务,并等待它们逐一执行完毕。这样做能够确保在系统停机前,所有已经提交到线程池的任务都得到了妥善的处理,从而避免了数据丢失或任务中断的风险。

值得注意的是,由于shutdownHook会回调Spring容器,我们可以利用Spring框架提供的DisposableBean接口来实现类似的效果。具体来说,我们可以实现DisposableBean接口的destroy方法,在这个方法中调用线程池的shutdown()方法,并随后等待线程池中的任务执行完毕。这样做的好处在于,我们能够利用Spring容器的生命周期管理特性,在Spring容器关闭时自动触发线程池的关闭操作,从而简化了代码逻辑,提高了系统的可维护性。

更进一步的是,由于我们使用的是Spring管理的线程池,我们可以直接将优雅停机的工作交给Spring来管理。Spring框架内部提供了丰富的容器关闭逻辑,包括对线程池等资源的优雅释放。我们只需按照Spring的规范进行配置和操作,即可享受到这一便利。通过查看Spring的内部源码,我们可以更深入地理解其线程池管理和优雅停机机制的实现原理,从而更好地应用这一特性来提升我们项目的稳定性和可靠性。

@Override
public void destroy() {
    shutdown();
}

/**
 * Perform a shutdown on the underlying ExecutorService.
 * @see java.util.concurrent.ExecutorService#shutdown()
 * @see java.util.concurrent.ExecutorService#shutdownNow()
 */
public void shutdown() {
    if (logger.isDebugEnabled()) {
        logger.debug("Shutting down ExecutorService" + (this.beanName != null ? " '" + this.beanName + "'" : ""));
    }
    if (this.executor != null) {
        if (this.waitForTasksToCompleteOnShutdown) {
            this.executor.shutdown();
        }
        else {
            for (Runnable remainingTask : this.executor.shutdownNow()) {
                cancelRemainingTask(remainingTask);
            }
        }
        awaitTerminationIfNecessary(this.executor);
    }
}

四、线程池使用

我们放进容器的线程池设置了beanName。

    @Bean(COMMON_EXECUTOR)
    @Primary
    public ThreadPoolTaskExecutor commonExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(200);
        executor.setThreadNamePrefix("test-executor-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());//满了调用线程执行,认为重要任务
        executor.initialize();
        return executor;
    }

业务需要用,也可以根据beanName取出想用的线程池。

    @Autowired
    @Qualifier(ThreadPoolConfig.MAIL_EXECUTOR )
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;

或者是直接在方法上加上异步注解@async

    @Async
    @Override
    public String test() {
       return "hello";
    }

五、异常捕获

搭建我们的项目的线程池,千万别忘了一点,就是线程运行抛异常了,要怎么处理。

public static void main(String[] args) {
    Thread thread =new Thread(()->{
        log.info("111");
        throw new RuntimeException("运行时异常");
    });
    thread.start();
}

这时异常并不会打印日志,只会在控制台输出。在Thread类中,会进行默认的异常处理,其实就是获取一个默认的异常处理器。默认的异常处理器是ThreadGroup实现的异常捕获方法。

如何捕获线程异常

我们要做的很简单,就是给线程添加一个异常捕获处理器,以后抛了异常,就给它转成error日志。这样才能及时发现问题。

Thread有两种成员变量:实例变量和类静态变量。都可以设置异常捕获。区别在于一个生效的范围是单个thread对象,一个生效的范围是全局的thread。

接下来,关于异常捕获:

在Java中,异常捕获通常是通过try-catch块来实现的,而不是通过类的属性来设置的。无论是类静态变量还是实例对象属性,它们本身并不直接“设置”异常捕获。但是,你可以在每个线程的执行逻辑中(通常是在run()方法中)使用try-catch块来捕获和处理可能抛出的异常。
对于单个Thread对象,你可以在它的run()方法内部使用try-catch块来捕获和处理该线程执行过程中可能抛出的异常。这样,异常捕获的范围就限制在这个特定的线程实例内。
如果你想要在全局范围内捕获和处理线程中抛出的异常,情况就复杂一些。Java的线程模型并没有提供直接的方式来全局捕获所有线程的异常。但是,你可以采取一些策略来实现类似的功能,例如:
使用一个统一的异常处理机制,例如通过UncaughtExceptionHandler,它可以为线程设置一个处理器来捕获未处理的异常。这样,你可以为特定的线程或所有线程设置一个全局的异常处理器。
使用线程池,并在提交任务时包装任务以捕获异常,然后将异常传递给其他线程或组件进行处理。
请注意,全局异常处理需要谨慎使用,因为它可能会隐藏问题并使得调试变得困难。通常,最好在每个线程内部处理其自己的异常,以便更准确地定位问题并采取相应的措施。

// null unless explicitly set
private volatile UncaughtExceptionHandler uncaughtExceptionHandler;

// null unless explicitly set
private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;

一般选择给每个thread实例都加一个异常捕获。

Thread thread = new Thread(() -> {
    log.info("111");
    throw new RuntimeException("运行时异常了");
});
Thread.UncaughtExceptionHandler uncaughtExceptionHandler =(t,e)->{
    log.error("Exception in thread ",e);
};
thread.setUncaughtExceptionHandler(uncaughtExceptionHandler);
thread.start();
线程池的异常捕获

我们工作中一般不直接创建对象,都用的线程池。这下要怎么去给线程设置异常捕获呢?

用线程池的ThreadFactory,创建线程的工厂,创建线程的时候给线程添加异常捕获。

private static ExecutorService executor = new ThreadPoolExecutor(1, 1,
    0L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue<Runnable>(500), 
    new NamedThreadFactory("refresh-ipDetail",null, false,
                           new MyUncaughtExceptionHandler()));

这是一个业务线程池,直接在工厂里添加一个异常捕获处理器就好了。它在创建thread的时候,会把这个异常捕获赋值给thread。如果是这么简单,那一切到这儿就结束了。由于Spring的封装,想要给线程工厂设置一个捕获器,可是很困难的。

    @Bean(COMMON_EXECUTOR)
    @Primary
    public ThreadPoolTaskExecutor commonExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(200);
        executor.setThreadNamePrefix("test-executor-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());//满了调用线程执行,认为重要任务
        executor.initialize();
        return executor;
    }
}

在这里插入图片描述

可以看到它自己实现了ThreadFactory。在CustomizableThreadFactory类的位置
在这里插入图片描述

点进去可以看见它内部封装好的创建线程的方法

    public Thread createThread(Runnable runnable) {
        Thread thread = new Thread(this.getThreadGroup(), runnable, this.nextThreadName());
        thread.setPriority(this.getThreadPriority());
        thread.setDaemon(this.isDaemon());
        return thread;
    }

就没有机会去设置一个线程捕获器。

它的抽象类ExecutorConfigurationSupport将自己赋值给线程工厂,提供了一个解耦的机会。
在这里插入图片描述
若我们决定替换当前的线程工厂,那么原本工厂所定义的线程创建方法将不再适用。这意味着,不仅仅是线程的创建,包括线程名、优先级等属性的设置都将需要重新考虑和调整。然而,我们的初衷仅仅是希望扩展线程的异常捕获功能,而不是完全颠覆现有的线程创建机制。
在这种情境下,一个经典的设计模式——装饰器模式——恰好能够派上用场。装饰器模式的核心思想是在不改变对象本身功能的基础上,动态地给对象添加一些职责。它允许我们通过包装对象来增加新的行为或状态,同时保持对象的接口不变。
为了应用这一模式,我们首先需要创建一个自定义的线程工厂类。这个类将接受Spring原始的线程工厂作为参数,并在其基础上进行扩展。在自定义线程工厂中,我们首先调用Spring线程工厂的线程创建方法,得到一个新的线程实例。随后,我们可以在这个线程实例上添加我们自己的扩展功能——即异常捕获的设置。
通过这种方式,我们既保留了Spring线程工厂原有的线程创建逻辑,又能够灵活地添加自己的异常捕获机制。这不仅符合开闭原则(对扩展开放,对修改封闭),也提高了代码的可维护性和可重用性。因此,装饰器模式完全适合我们这次的改动需求。

public class MyThreadFactory implements ThreadFactory {

    private ThreadFactory original;

    @Override
    public Thread newThread(Runnable r) {
        Thread thread = original.newThread(r);
        thread.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());//异常捕获
        return thread;
    }
}

class MyUncaughtExceptionHandler  implements Thread.UncaughtExceptionHandler {
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        log.error("Exception in thread {} ", t.getName(), e);
    }

}

第二步,替换spring线程池的线程工厂。

    @Bean(COMMON_EXECUTOR)
    @Primary
    public ThreadPoolTaskExecutor commonExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(200);
        executor.setThreadNamePrefix("test-executor-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());//满了调用线程执行,认为重要任务
        executor.setThreadFactory(new MyThreadFactory(executor));//替换spring线程池的线程工厂
        executor.initialize();
        return executor;
    }
}

一个完美的装饰器模式就这样完美地融入了我们的代码中,既实现了功能扩展,又保留了原有对象的接口和行为,真正做到了优雅与灵活并存。
至于为何选择使用Spring的线程池而非原生的线程池,这背后有多重考量。首先,Spring框架为我们提供了丰富的线程池配置选项和管理机制,使得我们可以更方便地根据应用需求调整线程池的大小、任务队列长度等参数,从而确保系统在高并发场景下能够稳定运行。

其次,Spring线程池还具备许多原生线程池所不具备的优雅特性。例如,Spring提供了线程池的优雅关闭功能,当应用需要停止时,它可以逐步关闭线程池,确保正在执行的任务能够完成后再停止线程,从而避免了任务中断或数据丢失的风险。此外,Spring线程池还支持与Spring容器集成,使得我们可以更方便地管理和监控线程池的状态和性能。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值