java - 统一管理项目的线程池

本文讲述了在Spring项目中为何需要统一管理线程池,如何自定义配置线程池及其参数含义,以及如何利用装饰器模式在不改变原线程池逻辑的情况下添加异常处理功能。
摘要由CSDN通过智能技术生成

问题引出

为什么需要统一管理线程池呢?因为频繁地自建线程会浪费CPU资源,可能导致OOM问题。另外,频繁地创建线程,销毁线程会给系统带来额外的开销。随着访问量的增加,系统会有崩溃的可能。因此,通常我们需要统一管理项目的线程池。

自建线程池

通常,这类配置我们都需要编写一个配置文件,统一放置在config包中,代码如下:

@Configuration
@EnableAsync
public class ThreadPoolConfig implements AsyncConfigurer {


    /**
     * 项目共用线程池
     */
    public static final String BADBOYCHAT_EXECUTOR = "badboychatExecutor";

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

    @Bean(BADBOYCHAT_EXECUTOR)
    @Primary
    public ThreadPoolTaskExecutor badboychatExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(200);
        // 设置优雅停机
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setThreadNamePrefix("badboychat-executor-");
        // 自定义的ThreadFactory,设置了UncaughtExceptionHandler,将异常日志打印出来,方便排查错误
        executor.setThreadFactory(new MyThreadFactory(executor));
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());//满了调用线程执行,认为重要任务
        executor.initialize();
        return executor;
    }
}

问:为什么要实现AsyncConfigurer接口?
答:实现AsyncConfigurer接口可以让注解@Async也使用我们统一管理的线程池,方便管理。

问:线程池的各个参数意义是什么?
答:CorePoolSize:表示核心线程数量。当线程池创建后,正常情况下线程池中会至少有corePoolSize个线程(除非设置了allowCoreThreadTimeout),即使这些线程目前都处于空闲状态。
MaxPoolSize:最大线程数量,线程池中允许存在的最大线程数,包括核心线程以及非核心线程。当应用请求的线程数超过这个数量时,将由RejectedExecutionHandler处理这种情况。
QueueCapacity:任务队列容量。如果线程池中的线程数量都在执行任务,那么新提交的任务会被暂存在队列中等待有空闲线程来执行。
WaitForTasksToCompleteOnShutdown:设置优雅停机,即保证任务队列中的任务执行完再停机,保证任务不丢失。
ThreadNamePrefix:设置线程池中的线程名的前缀,便于项目出措时排查问题。
ThreadFactory:是JUC包下的一个接口,主要是用来创建线程,对于这个参数,后面会详细说到。
RejectedExecutionHandler:任务拒绝策略,当线程池和任务队列都满了的时候,就会使用这个handler来处理多余的任务。"CallerRunsPolicy"表示这个任务由提交任务的线程自己去执行。其他的策略有AbortPolicy(直接抛出RejectedExecutionException异常)、DiscardPolicy(静默地抛弃任务, 不处理)、DiscardOldestPolicy(抛弃还在队列中等待的任务,并执行当前提交的任务)。
initialize: 初始化线程池,通常在设置完所有属性之后调用

自建线程池的使用

注意看:我们在自建线程池时,设置了BeanName,
在这里插入图片描述

因此使用时,直接使用注解@Autowired和注解@Qualifier(BeanName)即可

线程池异常捕获

线程中发生的异常都去了哪里,实际上如果我们没有捕获异常,异常会被线程抛出来,JVM会回调一个方法dispatchUncaughtException来处理这个异常,结果就是,异常信息从控制台输出。从控制台输出可不行,项目上线后,是看不到异常信息的,我们要让异常信息以日志的方式打印出来,这就需要我们为线程添加一个异常处理器。

Thread有两个属性,一个是类静态变量,一个是成员变量,区别是:类静态变量只对当前对象有效,成员变量对全局有效。通常,我们只需要捕获自己的异常就可以了,其他线程的异常我们不需要管。
在这里插入图片描述

Thread thread = new Thread(() -> {
    log.info("111");
    throw new RuntimeException("运行时异常了");li
});
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()));

但是我们现在使用的是Spring封装的线程池,想要给线程工厂设置一个异常捕获器非常困难,
但是我们可以发现我们自创建的线程池ThreadPoolTaskExecutor自己实现了ThreadFactory。从图中可以发现ThreadFactory继承自CustomizableThreadFactory,CustomizableThreadFactory实现了ThreadFactory,所以,ThreadPoolTaskExecutor也实现了ThreadFactory,
在这里插入图片描述
在这里插入图片描述
CustomizableThreadFactory中重写了newThread方法,点进去可以发现,完全没有机会给线程设置一个异常处理器。

在这里插入图片描述
仔细阅读源码可以发现,ThreadPoolTaskExecutor继承的抽象类ExecutorConfigurationSupport将自己赋值给了ThreadFactory,这提供了一个解耦的机会。
在这里插入图片描述

我们可以设置ThreadPoolTaskExecutor的ThreadFactory为自己编写的MyThreadFactory,然后在MyThreadFactory中给线程设置一个自定义的异常处理器。

@Slf4j
public class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        log.error("线程{}发生异常,异常信息为:{}",t.getName(),e.getMessage());
    }
}

这样真的可以了吗,如果我们将ThreadPoolTaskExecutor的ThreadFactory替换为自己编写的MyThreadFactory,那我们是不是要自己编写ExecutorConfigurationSupport中的所有代码逻辑呢(因为ThreadPoolTaskExecutor的ThreadFactory原本就是ExecutorConfigurationSupport),这也太复杂了,而且不科学!我们只是想在ExecutorConfigurationSupport的基础上再添加一个功能而已,并不想自己重新编写一大堆代码去替换掉他,说到这里,大家脑海中有没有浮现出一个答案呢?
当然是装饰器模式,装饰器模式不就是可以在不改变原有逻辑的前提下,添加额外的功能吗!在这里使用装饰器模式再合适不过了。
思路很清晰,那么现在就可以开始编写代码了。
首先编写一个MyThreadFactory实现接口ThreadFactory

@Slf4j
@AllArgsConstructor
public class MyThreadFactory implements ThreadFactory {

    private static final MyUncaughtExceptionHandler MY_UNCAUGHT_EXCEPTION_HANDLER = new MyUncaughtExceptionHandler();
    // 装饰器模式一般是通过关联来实现
    private ThreadFactory factory;
    @Override
    public Thread newThread(Runnable r) {
        Thread thread = factory.newThread(r);
        // 给线程设置异常处理器
        thread.setUncaughtExceptionHandler(MY_UNCAUGHT_EXCEPTION_HANDLER);
        return thread;
    }
}

然后用MyThreadFactory替换原来的线程工厂,这就可以捕获线程的异常了。
也就是代码中的

executor.setThreadFactory(new MyThreadFactory(executor));

到此,我们就介绍了如何统一管理项目中的线程池,欢迎大家留言讨论!

  • 28
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值