如何优雅地统一管理项目的线程池
1. 问题描述
有时我们写代码时会遇到这样的提示,例如下面的代码,每次暴露服务都会创建一个新的线程池,并且业务结束之后线程池也未随之销毁。
public void batchExportUrl() {
Thread task = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2500);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (ServiceUrl serviceUrl : PROVIDER_URL_SET) {
REGISTRY_SERVICE.register(serviceUrl);
log.info("[Server] export service {}",serviceUrl.getServiceName());
}
}
});
task.start();
}
类似这样频繁的创建、销毁线程和线程池,会给系统带来额外的开销。未经池化及统一管理的线程,则会导致系统内线程数上限不可控。
这种情况下,随着访问数增加,系统内线程数持续增长,CPU负载逐步提高。极端情况下,甚至可能会导致CPU资源被吃满,整个服务不可用。
为了解决上述问题,可增加统一线程池配置,替换掉自建线程和线程池。
2. 自建线程池
在ThreadPoolConfig
中,创建我们项目统一的线程池,并交给spring管理。
@Configuration
@EnableAsync
public class ThreadPoolConfig implements AsyncConfigurer {
/**
* 项目共用线程池
*/
public static final String COMMON_EXECUTOR = "commonExecutor";
@Override
public Executor getAsyncExecutor() {
// 返回共用线程池
return CommonExecutor();
}
/**
* 配置共用线程池
*/
@Bean(COMMON_EXECUTOR)
@Primary
public ThreadPoolTaskExecutor CommonExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 设置核心线程数为10
executor.setCorePoolSize(10);
// 设置最大线程数为10
executor.setMaxPoolSize(10);
// 设置队列容量为200
executor.setQueueCapacity(200);
// 设置线程名前缀为 "common-executor-"
executor.setThreadNamePrefix("common-executor-");
// 拒绝策略为CallerRunsPolicy
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 初始化线程池
executor.initialize();
return executor;
}
}
这里面做了两件事,创建一个统一线程池,并且还通过实现AsyncConfigurer
设置了@async
注解也使用我们的统一线程池,这样方便统一管理。
executor.setThreadNamePrefix("common-executor-");
设置线程池中每个线程的名称前缀。
- 这样做的好处是,在多线程调试和日志跟踪时(如排查cpu占用,死锁问题或者其他bug的时候)可以更容易地识别每个线程的来源。通过为每个线程指定一个特定的前缀,可以使线程名称更具有描述性,可以更轻松地识别线程的用途,从而提高代码的可读性和调试的方便性。
3. 优雅停机
当项目关闭的时候,需要通过jvm的shutdownHook回调线程池,等队列里任务执行完再停机。保证任务不丢失。
shutdownHook会回调spring容器,所以我们实现spring的DisposableBean
的destroy
方法也可以达到一样的效果,在里面调用executor.shutdown()
并等待线程池执行完毕。
由于我们用的就是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);
}
}
4. 异常捕获
线程运行抛异常了,要怎么处理?
在 Java 中,线程池中的任务抛出异常不会被主线程捕获,因为线程池中的线程是独立于主线程的。也就是说,子线程执行报错,不会打印错误日志,只会在控制台输出。
如果出了问题,却不打印error日志,那问题就被隐藏了,非常危险
解决方案
用线程池的ThreadFactory
,创建线程的工厂,创建线程的时候给线程添加异常捕获。
由于Spring的封装,想要给线程工厂设置一个异常捕获器,可是很困难的。如果我们把线程工厂换了,那么它的线程创建方法就会失效。线程名,优先级啥的全都得我们一并做了。而我们只是想扩展一个线程捕获。
于是我们就可以用到 装饰器模式,装饰器模式不会改变原有的功能,而是在功能前后做一个扩展点 。完全适合我们需求。
首先先写一个自己的线程工厂,把spring的线程工厂传进来。调用它的线程创建后,再扩展设置我们的异常捕获
import lombok.AllArgsConstructor;
import java.util.concurrent.ThreadFactory;
@AllArgsConstructor
public class MyThreadFactory implements ThreadFactory {
private final ThreadFactory factory;
@Override
public Thread newThread(Runnable r) {
Thread thread = factory.newThread(r);
thread.setUncaughtExceptionHandler(GlobalUncaughtExceptionHandler.getInstance());
return thread;
}
}
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class GlobalUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
private static final GlobalUncaughtExceptionHandler instance = new GlobalUncaughtExceptionHandler();
private GlobalUncaughtExceptionHandler() {
}
@Override
public void uncaughtException(Thread t, Throwable e) {
log.error("Exception in thread {} ", t.getName(), e);
}
public static GlobalUncaughtExceptionHandler getInstance() {
return instance;
}
}
第二步,替换spring线程池的线程工厂。
/**
* 配置共用线程池
*/
@Bean(COMMON_EXECUTOR)
@Primary
public ThreadPoolTaskExecutor CommonExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 设置核心线程数为10
executor.setCorePoolSize(10);
// 设置最大线程数为10
executor.setMaxPoolSize(10);
// 设置队列容量为200
executor.setQueueCapacity(200);
// 设置线程名前缀为 "common-executor-"
executor.setThreadNamePrefix("common-executor-");
// 拒绝策略为CallerRunsPolicy
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 设置线程工厂为自定义的CustomThreadFactory
executor.setThreadFactory(new MyThreadFactory());
// 初始化线程池
executor.initialize();
return executor;
}
5. 线程池使用
因为我们放进容器的线程池设置了beanName,所以可以根据beanName取出想用的线程池。例如下面代码:
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.concurrent.Executor;
@Component
public class YourClass {
@Resource(name = "commonExecutor") // 指定要注入的线程池的名称
private Executor commonExecutor;
public void yourMethod() {
// 使用注入的线程池执行任务
commonExecutor.execute(() -> {
// 执行你的任务代码
System.out.println("Executing a task using commonExecutor thread pool.");
});
}
}
或者是直接在方法上加上异步注解@async
over
快用到项目里吧!!!