问题引出
为什么需要统一管理线程池呢?因为频繁地自建线程会浪费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));
到此,我们就介绍了如何统一管理项目中的线程池,欢迎大家留言讨论!