今天看一视频,提到说 Spring默认的异步线程池比较简单,每次执行异步任务,都会新建一个线程进行处理,不会重复利用,所以在用Spring框架开发的时候,需要自定义异步线程池。第一次听到这个说法。遂开始百度。
百度 “Spring 默认的异步线程池的配置”
不到三秒看到了这么一个博客: https://www.cnblogs.com/zhaoyuan72/p/16347289.html
看到了 原文地址是从知乎来的,遂跳转到知乎:https://zhuanlan.zhihu.com/p/346086161。
我这个人有个毛病,干什么都想看到最原始的那个东西,比如听歌就要听原唱,不听改编翻唱,好吧,其实这样没必要。
作者开篇提到
上一篇分享了JDK自带的线程池 ThreadPoolExecutor 的配置和参数详解,然而我们实际开发中更多的是使用SpringBoot来开发,Spring默认也是自带了一个线程池方便我们开发,它就是ThreadPoolTaskExecutor,接下来我们就来聊聊Spring的线程池吧。
就是说 JDK自带的线程池是 ThreadPoolExecutor,而Spring框架又自我封装了一个线程池 ThreadPoolTaskExecutor。说实话,这个思想就是这样,JDK本身有自带的东西,很多框架觉得JDK的东西不好,都会自己二次改造,再来一个。
比如说JDK默认的序列化机制速度慢,生成的字节占用空间大,用的是java.io.ObjectInputStream 和 ObjectOutputStream。所以一般我们也不会用JDK的序列化机制,比如前后端交互我们用JSON,JSON又可以用FastJson或者Jackson等等。其他的场景如一个RPC框架我们可以用 Java 下面的最快的序列化工具 Kyro。(举例可能不恰当,但是就是这么个意思)
再比如说 Spring也有些东西,可能其他框架觉得它不太好,也会自己写一个。比如 StringUtils 这个类,一般开发中可能都会用 common.lang3的StringUtils,MP也会封装自己的 StringUtils,Hutool也会封装自己的StringUtils。
突然就理解了 common.lang 和 common.lang3 以及 fastjson 和fastjson2 的关系了。
commons.lang3 就是 common.lang 的3.x版本, 但是应该是一次大版本升级,所以重新搞成了一个artifact,fastjson2也是如此。
common.lang3的一个版本一定是从3.x开始的。fastjson2也一定是从2.x开始的
我们大致看一下 ThreadPoolExecutor。
参考了这篇文章。
https://blog.csdn.net/cy973071263/article/details/131484311 。
大致看完这个,开始看Spring的 ThreadPoolTaskExecutor。就看之前知乎的那一篇文章,作者先说的是 SimpleAsyncTaskExecutor,
于是我决定执行自己的demo
发现几个问题
1.没有如作者博客一样,我的demo线程复用了,没有一直创建,但是默认好像是8个线程
2.没有如博客作者一样打印出来 SimpleAsyncTaskExecutor 这个类
于是,去看了一下博客的日期,距今(2024年0925日)已经三年多了,Spring我目前用的都是5.3的版本了
看到了这个,可以去翻源码了,猜想是不是新版本Spring 默认的 concurrencyLimit 是8
看源码,全局搜 concurrencyLimit。
卧槽,那岂不是沙雕了。
又看一下我之前设置 的定时默认线程数是 8,但是我已经全部注释掉了呀
于是,开始去找新的博客看看。这次搜索的词条是:
Spring默认的异常线程池 SimpleAsyncTaskExecutor
并刻意去看2023、2024年的帖子,发现基本都说 默认的异步线程池 SimpleAsyncTaskExecutor 线程确实不可以重用。
感觉还是我本身项目代码的问题,我应该在某个地方设置了 最多可用线程数为8或者怎么样。
尝试1:既然注释没有效果,直接删掉 SchedulingConfig 这个类
发现还是一样的。
尝试2:去其他项目,运行这个demo
发现还是一样的 8 个线程在重用
则现在又有1种猜测
我加了 @Async 注解,到底有没有用 SimpleAsyncTaskExecutor 线程池
突然想起来了之前看 一个项目做的笔记。
说 @Async 注解的原理是
发现这里的 核心线程数确实是8 ,md破案了。
感觉根本没有用 SimpleAsyncTaskExecutor。
再去问 文心一言
当在Spring应用中使用
@Async
注解来声明异步方法时,如果没有明确指定线程池,那么默认情况下会使用SimpleAsyncTaskExecutor ,那如果是SpringBoot应用呢
在Spring Boot应用中,当使用@Async
注解来声明异步方法时,如果没有明确指定线程池,其默认行为会根据Spring Boot的版本有所不同。
早期版本
在Spring Boot的某些早期版本中(通常指2.1之前的版本),如果没有特别配置,@Async
注解标注的异步方法可能会默认使用SimpleAsyncTaskExecutor
。这个执行器每次执行一个任务时都会创建一个新的线程,而不是复用现有的线程,这可能导致在高并发场景下创建大量线程,进而引发性能问题。
2.1及以后版本
从Spring Boot 2.1版本开始,情况发生了变化。在Spring Boot 2.1及以后的版本中,@Async
注解默认使用的线程池不再是SimpleAsyncTaskExecutor
,而是ThreadPoolTaskExecutor
。这意味着,如果没有特别配置,异步方法将会在一个线程池中执行,该线程池具有核心线程数、最大线程数、队列容量等配置参数。
具体来说,ThreadPoolTaskExecutor
的默认配置通常包括:
- 核心线程数:8
- 最大线程数:
Integer.MAX_VALUE
- 队列容量:
Integer.MAX_VALUE
(使用LinkedBlockingQueue
) - 空闲线程保留时间:60秒
- 线程池拒绝策略:
AbortPolicy
(当线程池饱和时,尝试执行新任务会抛出异常)
自定义配置
尽管Spring Boot提供了默认的线程池配置,但在实际应用中,根据业务需求和系统负载,这些默认配置可能需要调整。开发者可以通过在application.yml
或application.properties
文件中添加相应的配置属性来自定义线程池的行为。
例如,要调整核心线程数、最大线程数和队列容量,可以在application.properties
中添加如下配置:
spring.task.execution.pool.core-size=10
spring.task.execution.pool.max-size=20
spring.task.execution.pool.queue-capacity=50
或者,在application.yml
中添加类似配置:
spring:task:execution:pool:core-size: 10max-size: 20queue-capacity: 50
结论
因此,在Spring Boot应用中,如果没有明确指定线程池,@Async
注解默认使用的线程池从Spring Boot 2.1版本开始是ThreadPoolTaskExecutor
,而不是SimpleAsyncTaskExecutor
。这提供了更好的线程复用和资源管理能力,但在实际应用中仍需要根据需要进行适当的配置调整。
再点进去
确实需要自定义异步线程池。
我之前的demo默认走的是 ThreadPoolTaskExecutor的配置。
但是这里我就好奇了,那在Spring 框架里面默认的线程池配置是通过哪个类实现的???
ai得出这个答案,我就不单独去验证Spring框架下面,@Scheduled 注解的行为了,毕竟现在都是基于SpringBoot开发。
如何自定义异步线程池,参考了 https://blog.csdn.net/kdzandlbj/article/details/139046266 的第二种方式。
异步线程池定义如图所示
启动项目,发现确实生效了,最大的执行线程就是5
但是这里有个问题,为什么 springBoot(这应该是SpringBoot决定的吧)的log打印,好像是打印 线程名.subtring(str.length-15)
稍等讨论这个问题,我想知道这个
executor.setThreadNamePrefix("AsyncThreadPool-t-");
线程工厂是如何把这个 线程名前缀加在线程上的.
首先要认知到一个问题,任何框架的线程池肯定都是基于JDK线程池的,而JDK线程池里面的线程都是由线程工厂创建的 也就是 ThreadFactory。所以,直接去找线程工厂。
点进去 CustomizableThreadFactory 看一看
再来说 SpringBoot输出的为什么是 substring的ThreadName。
找了一下感觉乱七八糟的,没什么章法,直接百度吧。
百度了一圈没有说什么源码的,都是说配置的,没劲,我自己debug看吧。
但是代码到下面就走完了。
事前先百度了一下。
ok,到这里 线程日志打印截断15个字符就结束了。