前言
在遥远的过去,我有一段令人不快的回忆。那种被问题所困扰的感觉一直挥之不去。
并非别的原因,是因为有位同事过度滥用了线程池的方式。确切地说,就是下面这篇文章。
夺命故障 ! 炸出了投资人!
我觉得有必要简单地重述一下。主要问题在于开发人员在每次方法调用时都创建了一个独立的线程池进行处理。这样一来,如果请求量增加,整个操作系统的负载将过高,导致所有业务都无法正常响应。
我一直认为这是一个相当罕见的低级错误,发生频率非常低。然而,随着类似故障的增多,xjjdog意识到这是一个普遍存在的问题。
徒劳地追求异步性能优化,却导致整个业务无法正常运行,这种结果着实令人尴尬。
1.Spring的异步代码
作为Java领域中备受推崇的框架,Spring以其高度封装的API深受开发人员的青睐。按照语义化编程的理念,只要在语言层面上满足一定的条件,我们就可以将其应用其中,比如@Async注解。
我一直对于开发人员为何敢于使用@Async注解感到困惑,因为涉及到多线程的内容,即便是手动创建线程,也需要谨慎对待,以免干扰操作系统的正常运行。@Async这样的黑盒,真的能如此顺畅地使用吗?
我们可以通过调试代码来验证一下,让代码运行一段时间,看看会发生什么。
首先,生成一个小小的项目,然后在主类上加上必须的注解。嗯,别忘了这一环,否则你后面加的注解将没什么用处。
@SpringBootApplication
@EnableAsync
public class DemoApplication {
创造一个带@Async注解的方法。
@Component
public class AsyncService {
@Async
public void async(){
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread());
}catch (Exception ex){
ex.printStackTrace();
}
}
}
然后,做一个对应的test接口,访问时会调用这个async方法。
@ResponseBody
@GetMapping("test")
public void test(){
service.async();
}
访问时,直接打个断点,即可获取执行异步线程的线程池。
可以观察到,在异步任务中使用了一个线程池,其核心线程数(corePoolSize)设置为8,并采用了无界队列(LinkedBlockingQueue)作为阻塞队列。一旦使用了这种组合方式,最大线程数就变得没有意义了,因为超过8个线程的任务都会被放入无界队列中。这导致下面的代码实际上没有什么作用。
throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, var4);
假如你的网站承受着非常高的访问量,那么所有这些任务都会积压在LinkedBlockingQueue中。在较好的情况下,这些任务的执行会受到显著的延迟;而在较糟糕的情况下,任务的数量过多将直接导致内存溢出(OutOfMemoryError)的问题!
也许有些人会建议自己指定另一个ThreadPoolExecutor,然后使用@Async注解进行声明。但我认为,提出这种建议的人要么是非常有实力,要么是很少经历过团队中其他人的不规范代码的洗礼。
2.是SpringBoot救了你
SpringBoot是个好东西。
在TaskExecutionAutoConfiguration中,通过生成ThreadPoolTaskExecutor的Bean,来提供默认的Executor。
@ConditionalOnMissingBean({Executor.class})
public ThreadPoolTaskExecutor applicationTaskExecutor(TaskExecutorBuilder builder) {
return builder.build();
}
也就是我们上面所说的那个。如果没有SpringBoot的助力,Spring将默认使用SimpleAsyncTaskExecutor。
参见org.springframework.aop.interceptor.AsyncExecutionInterceptor。
@Override
@Nullable
protected Executor getDefaultExecutor(@Nullable BeanFactory beanFactory) {
Executor defaultExecutor = super.getDefaultExecutor(beanFactory);
return (defaultExecutor != null ? defaultExecutor : new SimpleAsyncTaskExecutor());
}
这就是Spring大仙所干的事。
SimpleAsyncTaskExecutor类的设计方式实在是有些不太理想,因为每次执行任务时都会创建一个独立的线程,根本没有复用线程池的概念。假设你的系统每秒处理1000个请求,并且采用异步任务执行,那么每秒将会创建1000个线程!
这种设计方式对系统的性能和资源消耗造成了相当大的负担。实际上,线程的创建和销毁是非常耗费资源的操作,频繁地创建大量线程会导致系统的负载加重,甚至可能引发性能问题和资源竞争。
这明显是想要累死操作系统的节奏。
protected void doExecute(Runnable task) {
Thread thread = (this.threadFactory != null ? this.threadFactory.newThread(task) : createThread(task));
thread.start();
}
3.End
对于有经验的开发者来说,这种直接使用new Thread的方式来处理任务确实是一个令人担忧的做法。然而,令人意外的是,Spring框架本身在一些地方仍然使用了这种方式,其中包括一些广泛使用的组件,如AsyncRestTemplate。
这种情况引发了一些讨论和质疑,因为在并发环境下,频繁地创建和销毁线程可能导致系统资源的浪费和性能下降。很多开发者希望能够找到更好的替代方案,以改进异步任务的处理方式,并提高系统的性能和稳定性。
这引发了一些关于设计决策的争议,特别是在一些与Redis相关的列表中发现了这个类的使用。这个类的设计使得任务的执行变得难以控制。
对于一些开发者来说,这样的API设计似乎有些不合理。看到这个API,他们对于Spring框架的设计感到困惑。
这种设计可能隐藏着更深层次的bug。比如,在一些框架中,直接使用了@EventListener注解来实现所谓的事件驱动模式,而该注解默认使用了SimpleAsyncTaskExecutor,这可能会导致一些潜在的问题。
建议将SimpleAsyncTaskExecutor加入API的黑名单或者问题清单中。
创建线程本身并不难,为什么非要依赖于Spring提供的线程呢?有时候真的很难理解,为什么要暴露这样的接口,它的目的是什么。
就算是原生的线程池,我们都还没完全掌握,为什么还要额外添加一层封装呢?这只会增加责任和问题的难度!