Spring Boot项目中的线程-1
Java 8
Spring Boot v2.7.3
Windows 11
--
前言
main线程,每个spring boot项目都有的。除了main线程,访问Web接口时还会有处理HTTP请求的线程。
下面是一个spring boot的web项目 的依赖项:threadpool
org.springframework.boot:spring-boot-starter-web发布于博客园
org.springframework.boot:spring-boot-starter-actuator
org.projectlombok:lombok
启动:有 main 线程的日志输出(默认info级别)
访问 http://localhost:10000/,此时出现名为“io-10000-exec-3”的线程:
从上面可以知道,已经有2个线程了。此时JVM进程中还有没有其它线程呢?有。发布于博客园
使用 jvisualvm.exe 查看JVM的线程
运行 jvisualvm.exe (JAVA_HOME/bin 目录下),选择 项目threadpool 的JVM检查:
查看线程:发布于博客园
可以看到,服务运行起来后,main线程没有了,但是,处理Web请求的 “io-10000-exec-3”线程还在,不过,其全名是 “http-nio-10000-exec-3”。
使用 jstack 命令查看JVM的线程
jps命令找到进程号,再使用 jstack 命令查看线程情况。下面是 找到的 线程名称所在列:发布于博客园
这里可以看到 更多的线程——包括Java的GC线程。当然,jvisualvm 工具看到的更加直观。
各个线程分别有什么用处,暂不清楚,TODO
关闭项目,此时出现一个名为 “n(32)-127.0.0.1”的线程,应该是某个 RMI TCP Conection开头的线程。
2022-09-14 20:55:39.733 INFO 11160 --- [n(32)-127.0.0.1] inMXBeanRegistrar$SpringApplicationAdmin : Application shutdown requested.
2022-09-14 20:55:40.664 INFO 11160 --- [n(32)-127.0.0.1] o.apache.catalina.core.StandardService : Stopping service [Tomcat]
2022-09-14 20:55:40.667 INFO 11160 --- [n(32)-127.0.0.1] o.a.c.c.C.[Tomcat].[localhost].[/] : Destroying Spring FrameworkServlet 'dispatcherServlet'
使用@Async注解产生的线程
在启动类使用 @EnableAsync ,建立两个接口:发布于博客园
/api/1/get1 调用 异步方法
/api/1/get2 调用 同步方法
代码如下:
@RestController
@RequestMapping(value = {"/api/1"})
@Slf4j
public class FirstController {
@Autowired
private FirstService firstService;
@GetMapping(value = {"/get1"})
public String get1() {
log.info("call get1#1 now={}", new Date());
// 异步执行
firstService.get1Async();
log.info("call get1#2 now={}", new Date());
return "1、" + new Date();
}
@GetMapping(value = {"/get2"})
public String get2() {
log.info("call get2#1 now={}", new Date());
// 同步执行
firstService.get2Sync();
log.info("call get2#2 now={}", new Date());
return "2、" + new Date();
}
}
@Service
@Slf4j
public class FirstService {
@Async
public void get1Async() {
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
log.error("get2Sync InterruptedException");
return;
}
log.info("get1Async Thread name={}", Thread.currentThread().getName());
}
public void get2Sync() {
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
log.error("get2Sync InterruptedException");
return;
}
log.info("get2Sync Thread name={}", Thread.currentThread().getName());
}
}
分表调用两个测试接口,其中,接口 /api/1/get2 需要等待10秒才返回结果;日志显示,调用 /api/1/get1 时,新出现了一个名为 task-1 的线程。发布于博客园
疑问:
名为 task-1 的线程 是怎么来的呢?
检查 jvisualvm.exe 的线程,task-1 在执行完任务后等待一段时间就消失了。
task-1线程 是 在方法使用了 @Async 注解产生的。不过,这个线程(默认)不是来自于线程池,而是直接新建的一个线程(需要查看源码或相关博文)。
检查 启动后的bean列表,发现使用 @EnableAsync 多了以下两个Bean:
org.springframework.scheduling.annotation.ProxyAsyncConfiguration
org.springframework.context.annotation.internalAsyncAnnotationProcessor
注,比较工具为 SourceGear DiffMerge 4.2.0,开源软件。发布于博客园
让 @Async 的方法用上线程池:使用ThreadPoolTaskExecutor
配置默认线程池,建立一个名为 taskExecutor 的Bean(注意,可以是其它名字):
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@Configuration
public class ThreadPoolConfig {
@Bean("taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.initialize();
taskExecutor.setCorePoolSize(20);
taskExecutor.setQueueCapacity(200);
taskExecutor.setMaxPoolSize(40);
// 拒绝策略
taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 线程名前缀
taskExecutor.setThreadNamePrefix("app-th-");
return taskExecutor;
}
}
启动服务。访问 接口 /api/1/get1,从日志可以看到,其使用了 新建的线程池taskExecutor。发布于博客园
修改线程池的名称为 非 taskExecutor,比如,appTaskExecutor。修改后,再次调用 接口 /api/1/get1:还是使用了这个线程池
建立两个名称不为 taskExecutor 的线程池Bean
线程池 appTaskExecutorOne、appTaskExecutorTwo:
@Configuration
public class ThreadPoolConfig {
@Bean("appTaskExecutorOne")
public Executor appTaskExecutorOne() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.initialize();
taskExecutor.setCorePoolSize(20);
taskExecutor.setQueueCapacity(200);
taskExecutor.setMaxPoolSize(40);
// 拒绝策略
taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 线程名前缀
taskExecutor.setThreadNamePrefix("app2-th-");
return taskExecutor;
}
@Bean("appTaskExecutorTwo")
public Executor appTaskExecutorTwo() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.initialize();
taskExecutor.setCorePoolSize(20);
taskExecutor.setQueueCapacity(200);
taskExecutor.setMaxPoolSize(40);
// 拒绝策略
taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 线程名前缀
taskExecutor.setThreadNamePrefix("app2-th-");
return taskExecutor;
}
}
此时,调用接口/api/1/get1,没有使用任何一个线程池,并且提示:
AnnotationAsyncExecutionInterceptor : More than one TaskExecutor bean found within the context,
and none is named 'taskExecutor'. Mark one of them as primary or name it 'taskExecutor'
(possibly as an alias) in order to use it for async processing:
[appTaskExecutorOne, appTaskExecutorTwo]
解决方法有好几种。发布于博客园
比如,再建一个 名为taskExecutor 的线程池即可。也可以 使用 @Async 注解的 value属性 指定一个线程池:
用完线程池的20个核心线程:快速调用 接口 /api/1/get1。核心线程不会自动关闭。发布于博客园
遗留问题:关闭服务时,出现下面的错误,是什么原因导致的呢?怎么解决?和线程池的线程关闭有关系。
The web application [ROOT] appears to have started a thread named [app2-th-1] but has
failed to stop it. This is very likely to create a memory leak. Stack trace of thread:
sun.misc.Unsafe.park(Native Method)
解决方案:TODO
小结
从上面来看,使用 org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor 建立线程池的方式很简单,还可以指定线程前缀。不过,线程的数据设置、拒绝策略设置等,作者还没彻底搞清楚,要考虑任务类型是不是IO消耗、CPU消耗等类型。
对于默认线程池taskExecutor的建立,除了上面的方式,Spring还提供了另外的方式。详情见参考资料或其它博文、官方文档。
除了使用 spring 的 ThreadPoolTaskExecutor 类建立线程池,也可以使用 Java自身的建立线程池的方式——Java基础,忘记了可以复习下。发布于博客园
疑问:
一个JVM有多少个线程是合适的呢?
建立多少个线程池是合适的呢?
和CPU、堆内存等有什么关系?
拒绝策略要怎么限制?
是不是要和流量控制结合起来使用呢?
参考资料
1、Spring异步请求与异步调用
https://www.jianshu.com/p/5bfce56db995
2、SpringBoot关于@Async线程池配置
https://blog.csdn.net/weixin_42272869/article/details/123082657
Quote:“关于修改 @Async默认的线程池 ,我们仅仅需要实现一个 AsyncConfigurer 类”
3、