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