Spring的线程机制
Spring框架本身不管理线程,它依赖于运行Spring应用程序的服务器来处理并发。
在典型的Spring Web应用程序中,当一个HTTP请求到达服务器时,服务器(如Tomcat
)将从其线程池中选取一个线程来处理这个请求。这个线程将负责执行整个请求的处理流程,包括调用Spring的Controller
方法、进行业务逻辑
处理、访问数据库
等。在此过程中,Spring并不进行任何线程管理或调度。
在整个处理流程完成后,服务器将处理结果返回给客户端,然后线程返回到服务器的线程池中,等待处理下一个请求。
因此,在不进行任何线程配置的情况下,Spring应用程序的并发处理能力主要取决于运行该应用程序的服务器的线程池配置
,包括线程池的大小、线程调度策略等。
需要注意的是,即使Spring不管理线程,也需要注意线程安全问题。例如,如果Spring Bean被多个线程共享,且包含可变状态,则需要保证其线程安全。另一方面,Spring提供了一些机制(如@Async
注解、TaskExecutor
接口等)来帮助进行显式的并发处理。
@Async
概念原理
@Async
注解是Spring框架提供的一种异步调用机制
它的实现原理如下
- 在Spring容器启动时,会扫描所有的
@Service、@Controller、@Component
等注解标注的Bean,查找其中的@Async
注解。 - 当发现一个方法上标注了
@Async
注解时,Spring会使用CGLIB动态代理机制为该方法生成一个代理对象。 - 代理对象会将异步方法的调用转换为一个Runnable任务,并提交给线程池执行。
- 当任务执行完成后,代理对象会将异步方法的返回值或异常信息封装到一个Future对象中,供调用方使用。
具体来说,@Async注解的实现依赖于以下几个组件:
TaskExecutor
:用来执行异步任务的线程池。默认使用SimpleAsyncTaskExecutor
,一个没有最大数量限制的线程池,每次调用都会创建一个新的线程,并发大的时候会产生严重的性能问题。AsyncAnnotationBeanPostProcessor
:用来扫描Bean中的@Async
注解,并为标注了该注解的方法生成代理对象。AsyncUncaughtExceptionHandler
:用来处理异步任务执行中出现的异常。可以自定义实现该接口来定制异常处理逻辑。
总体来说,@Async
注解的实现原理主要是基于动态代理和线程池机制来实现的。它可以大大简化异步调用的代码编写,提高应用程序的并发性能和资源利用率。
使用
@Async
是Spring内置注解
,用来处理异步任务,在SpringBoot
中同样适用,且在SpringBoot
项目中,除了boot本身的starter外,不需要额外引入依赖。
而要使用@Async
,需要在 主启动类或配置类上加上@EnableAsync
主动声明来开启异步方法。
@EnableAsync
@SpringBootApplication
public class Application {
//...
}
或者
@EnableAsync
@Configuration
public class config {
//...
}
在方法或类上加@Async
,方法上加就是这个方法开启异步,类上就是这个类所有方法都异步
@Component
public class MyAsyncTask {
@Async
public void asyncCpsItemImportTask(Long platformId, String jsonList){
//...具体业务逻辑
}
}
代码示例
- 在配置类中配置线程池并开启异步注解:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "asyncExecutor")
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10); // 核心线程数
executor.setMaxPoolSize(20); // 最大线程数
executor.setQueueCapacity(100); // 队列容量,如果此数字大于0,使用队列LinkedBlockingQueue
executor.setThreadNamePrefix("AsyncThread-"); // 线程名称前缀
executor.initialize();//初始化线程池
return executor;
}
}
- 在需要异步执行的方法上使用@Async注解,并指定使用的线程池:
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class MyService {
@Async("asyncExecutor")
public void doSomethingAsync() {
// 异步执行的方法体
}
}
在上述示例中,通过在配置类中创建名为"asyncExecutor"的ThreadPoolTaskExecutor
Bean,并使用@EnableAsync
注解启用异步支持。
然后,在需要异步执行的方法上使用@Async注解,并指定使用的线程池名为"asyncExecutor"。
通过这样的配置,就可以在Spring Boot项目中使用ThreadPoolExecutor线程池进行异步处理。可以根据实际需求调整线程池的核心线程数、最大线程数、队列容量等参数,以满足项目的并发需求。
失效情况
在以下情况下,@Async
注解可能会失效:
异步方法与调用方法在同一个类中
:Spring通过基于代理的方式实现@Async注解的功能,而基于代理的方式只能在不同的类之间才能生效。如果异步方法和调用方法在同一个类中,Spring无法通过代理来增强方法,因此@Async注解会失效。异步方法没有被Spring容器管理
:异步方法需要被Spring容器管理,才能使@Async注解生效。如果异步方法没有被纳入Spring容器中,那么@Async注解将不会生效。比如spring无法扫描到异步类,没加注解@Async 或 @EnableAsync注解异步方法使用了final方法
:如果异步方法使用了final修饰符,那么@Async注解将会失效。jdk动态代理要求有接口,通过重写方法进行代理增强;而cglib通过继承目标类进行代理增强,但final修饰方法无法被重写和继承,所以失效。异步方法不是public方法
:注解@Async的方法不是public方法异步方法的返回值不是void或者Future
:注解@Async的返回值只能为void或者Future异步方法是static的
:注解@Async方法使用static修饰也会失效异步方法没有正确配置线程池
:在使用@Async注解时,需要配置线程池,如果没有正确配置线程池,那么异步方法将无法执行。
需要注意的是,以上情况下@Async注解失效的原因并不是@Async本身的问题,而是因为某些限制条件或配置错误导致无法实现异步调用。确保异步方法在不同类中被调用、被Spring容器管理,并且正确配置线程池,通常可以避免@Async失效的问题。
效果展示
无返回值方法异步
一个请求内部并行执行两个任务
如下,一个/user
请求到来,两个ttl1()
并行执行,而不是等上面的结束再执行下一个
配置类
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "asyncExecutor")
public ThreadPoolTaskExecutor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数
executor.setCorePoolSize(10);
// 最大线程数
executor.setMaxPoolSize(100);
// 队列容量,如果此数字大于0,使用队列LinkedBlockingQueue
executor.setQueueCapacity(100);
// 线程名称前缀
executor.setThreadNamePrefix("WebmvcThread-");
//初始化
executor.initialize();
return executor;
}
}
Controller
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private TtlTool ttlTool;
//localhost:8080/user/ttl
@RequestMapping("/ttl")
public void ttl() throws InterruptedException{
ttlTool.ttl1();
ttlTool.ttl1();
}
}
异步方法
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
@Component
public class TtlTool {
//volatile的作用是: 线程对副本变量进行修改后,其他线程能够立刻同步刷新最新的数值。这个就是可见性.
private volatile Integer i = 0;
@Async("asyncExecutor")
public void ttl1() throws InterruptedException{
this.i = i+1;
int j = this.i;
System.out.println("第"+i+"个异步方法被 "+Thread.currentThread().getName()+" 开始执行于 "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
Thread.sleep(3000);
System.out.println("第"+j+"个异步方法被 "+Thread.currentThread().getName()+" 结束执行于"+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
}
}
执行结果
使用了异步
@Async
的执行结果:
第1个异步方法被 AsyncThread-1 开始执行于 2023-09-23 10:17:04
第2个异步方法被 AsyncThread-2 开始执行于 2023-09-23 10:17:04
第1个异步方法被 AsyncThread-1 结束执行于2023-09-23 10:17:07
第2个异步方法被 AsyncThread-2 结束执行于2023-09-23 10:17:07
如果去掉异步执行,将
TtlTool
的@Async("asyncExecutor")
注解去掉,执行结果就是
第1个异步方法被 http-nio-8080-exec-2 开始执行于 2023-09-23 10:25:05
第1个异步方法被 http-nio-8080-exec-2 结束执行于2023-09-23 10:25:08
第2个异步方法被 http-nio-8080-exec-2 开始执行于 2023-09-23 10:25:08
第2个异步方法被 http-nio-8080-exec-2 结束执行于2023-09-23 10:25:11
可见在使用了异步@Async
之后,第1个与第2个异步任务都是在10:17:04同一时间执行的,而不是等3秒在执行,证明异步执行成功
带返回值的方法异步
需要使用回调函数,将异步结果返回
返回值类
public class Demo {
private String one;
private String two;
private String three;
private String four;
public String getOne() {
return one;
}
public void setOne(String one) {
this.one = one;
}
public String getTwo() {
return two;
}
public void setTwo(String two) {
this.two = two;
}
public String getThree() {
return three;
}
public void setThree(String three) {
this.three = three;
}
public String getFour() {
return four;
}
public void setFour(String four) {
this.four = four;
}
}
配置类
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "asyncExecutor")
public ThreadPoolTaskExecutor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数
executor.setCorePoolSize(10);
// 最大线程数
executor.setMaxPoolSize(100);
// 队列容量,如果此数字大于0,使用队列LinkedBlockingQueue
executor.setQueueCapacity(100);
// 线程名称前缀
executor.setThreadNamePrefix("WebmvcThread-");
//初始化
executor.initialize();
return executor;
}
}
异步方法
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CompletableFuture;
@Component
public class TtlTool {
@Async("asyncExecutor")
public CompletableFuture<Demo> ttl1() throws InterruptedException{
System.out.println("异步方法被 "+Thread.currentThread().getName()+" 开始执行于 "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
Thread.sleep(5000);
Demo d = new Demo();
d.setOne("1");
d.setTwo("2");
d.setThree("3");
d.setFour("4");
System.out.println("异步方法被 "+Thread.currentThread().getName()+" 结束执行于"+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
CompletableFuture<Demo> cd = CompletableFuture.completedFuture(d);
return cd;
}
}
Controller
import com.example.bean.Demo;
import com.example.service.TtlTool;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
@RestController
public class IndexController {
@Autowired
private TtlTool ttlTool;
@RequestMapping("/user/{name}")
public DeferredResult<Demo> demo(@PathVariable String name) throws InterruptedException, ExecutionException {
System.out.println("Controller方法被 "+Thread.currentThread().getName()+" 开始执行于 "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
//用于封装异步返回值给前端
DeferredResult<Demo> deferredResult = new DeferredResult<>();
CompletableFuture<Demo> future = null;
try {
future = ttlTool.ttl1();
} catch (InterruptedException e) {
e.printStackTrace();
}
/**
* thenApply 表示某个任务执行完成后执行的动作,即回调方法:
* 会在ttlTool.ttl1()的方法执行结束后,thenApply才会触发,与ttl1()使用同一个线程。
* 如果用thenAcceptAsync,则会用新线程进行回调处理
* */
future.thenAccept(result -> {
// 在异步方法执行完成后,使用回调函数处理结果
System.out.println("回调方法被 "+Thread.currentThread().getName()+" 执行于 "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
deferredResult.setResult(result); // 设置返回结果
});
System.out.println("Controller方法被 "+Thread.currentThread().getName()+" 结束执行于 "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
System.out.println();
return deferredResult;
}
}
执行结果
同时发送两次"http://localhost:8081/user/asd
"请求,结果如下:
Controller方法被 http-nio-8081-exec-1 开始执行于 2023-10-20 10:07:58
Controller方法被 http-nio-8081-exec-1 结束执行于 2023-10-20 10:07:58
异步方法被 WebmvcThread-1 开始执行于 2023-10-20 10:07:58
Controller方法被 http-nio-8081-exec-9 开始执行于 2023-10-20 10:07:58
Controller方法被 http-nio-8081-exec-9 结束执行于 2023-10-20 10:07:58
异步方法被 WebmvcThread-2 开始执行于 2023-10-20 10:07:58
异步方法被 WebmvcThread-1 结束执行于2023-10-20 10:08:03
回调方法被 WebmvcThread-1 执行于 2023-10-20 10:08:03
异步方法被 WebmvcThread-2 结束执行于2023-10-20 10:08:03
回调方法被 WebmvcThread-2 执行于 2023-10-20 10:08:03
http-nio-8081-exec-1
与http-nio-8081-exec-9
是tomcat
同时开启的两个线程,用于处理两个同时到来的/user/asd
请求。证明请求是异步执行的WebmvcThread-1
是被http-nio-8081-exec-1
所调用的执行异步方法ttl1()
的线程,WebmvcThread-2
是被http-nio-8081-exec-9
所调用的执行异步方法ttl1()
的线程。WebmvcThread-1
与WebmvcThread-2
的开始执行时间一致,证明ttl()方法是异步执行WebmvcThread-1
与WebmvcThread-2
的结束执行时间是在开始时间的5秒后,对应异步方法中的休眠5秒。- 由于回调方法
thenAccept
,WebmvcThread-1
与WebmvcThread-2
执行结束后没有被释放,而是将结果返回。证明异步回调成功
请求异步
有返回值,同一个url,多次请求的异步执行
默认情况下,请求的异步是tomcat
自动实现的,tomcat
中自带线程池,当多个一摸一样的请求到来时,tomcat
会启动多个线程同时处理,这里使用@Async
来做演示
可以在yaml
做tomcat
的配置:
server:
port: 8081
tomcat:
# tomcat的URI编码
uri-encoding: UTF-8
# 连接数满后的排队数,默认为100
accept-count: 1000
threads:
# tomcat最大线程数,默认为200
max: 800
# Tomcat启动初始化的线程数,默认值10
min-spare: 100
#最大连接数
max-connections: 100
如下,多个
/user
请求同时到来,Controller异步进行处理,并返回结果给浏览器
返回值类型
public class Demo {
private String one;
private String two;
private String three;
private String four;
public String getOne() {
return one;
}
public void setOne(String one) {
this.one = one;
}
public String getTwo() {
return two;
}
public void setTwo(String two) {
this.two = two;
}
public String getThree() {
return three;
}
public void setThree(String three) {
this.three = three;
}
public String getFour() {
return four;
}
public void setFour(String four) {
this.four = four;
}
}
配置
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "asyncExecutor")
public ThreadPoolTaskExecutor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数
executor.setCorePoolSize(10);
// 最大线程数
executor.setMaxPoolSize(100);
// 队列容量,如果此数字大于0,使用队列LinkedBlockingQueue
executor.setQueueCapacity(100);
// 线程名称前缀
executor.setThreadNamePrefix("WebmvcThread-");
//初始化
executor.initialize();
return executor;
}
}
异步方法
@Component
public class TtlTool {
@Async("asyncExecutor")
public CompletableFuture<Demo> ttl1() throws InterruptedException{
System.out.println("异步方法被 "+Thread.currentThread().getName()+" 开始执行于 "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
Thread.sleep(3000);
Demo d = new Demo();
d.setOne("1");
d.setTwo("2");
d.setThree("3");
d.setFour("4");
System.out.println("异步方法被 "+Thread.currentThread().getName()+" 结束执行于"+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
CompletableFuture<Demo> cd = CompletableFuture.completedFuture(d);
return cd;
}
}
默认的Controller
使用
tomcat
的线程进行异步处理,controller
请求会被tomcat
线程池中的线程处理,而异步方法ttl1()
会被我们配置的@Async
线程池处理
@RestController
public class IndexController {
@Autowired
private TtlTool ttlTool;
@RequestMapping("/user/{name}")
public DeferredResult<Demo> demo(@PathVariable String name) throws InterruptedException, ExecutionException {
System.out.println("Controller方法被 "+Thread.currentThread().getName()+" 开始执行于 "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
DeferredResult<Demo> deferredResult = new DeferredResult<>();
CompletableFuture<Demo> future = null;
try {
future = ttlTool.ttl1();
} catch (InterruptedException e) {
e.printStackTrace();
}
future.thenAccept(new Consumer<Demo>() {
@Override
public void accept(Demo result) {
// 在异步方法执行完成后,使用回调函数处理结果
System.out.println("Async Result: " + result);
deferredResult.setResult(result); // 设置返回结果
}
});
System.out.println("Controller方法被 "+Thread.currentThread().getName()+" 结束执行于 "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
System.out.println();
return deferredResult;
}
}
使用@Async的Controller
实际开发并不需要使用这种配置,只是演示一下,这样也能用,但是没法将数据返回前端。
原因是:线程切换了,请求的处理被
tomcat
线程交给了我们配置的@Async
线程,tomcat
线程转而被释放,当@Async
线程处理完请求后,返回值无法还给原来调用的tomcat
线程了
@RestController
public class IndexController {
@Autowired
private TtlTool ttlTool;
@RequestMapping("/user/{name}")
@Async
public DeferredResult<Demo> demo(@PathVariable String name) throws InterruptedException, ExecutionException {
System.out.println("Controller方法被 "+Thread.currentThread().getName()+" 开始执行于 "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
DeferredResult<Demo> deferredResult = new DeferredResult<>();
CompletableFuture<Demo> future = null;
try {
future = ttlTool.ttl1();
} catch (InterruptedException e) {
e.printStackTrace();
}
future.thenAccept(new Consumer<Demo>() {
@Override
public void accept(Demo result) {
// 在异步方法执行完成后,使用回调函数处理结果
System.out.println("Async Result: " + result);
deferredResult.setResult(result); // 设置返回结果
}
});
System.out.println("Controller方法被 "+Thread.currentThread().getName()+" 结束执行于 "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
System.out.println();
return deferredResult;
}
}
执行结果
**注意一个问题:**在开启了Controller异步后,在某些浏览器上对该Controller,进行同一时间的多次请求时,可能还是顺序执行,没有达到异步效果,这不是代码问题,而是浏览器原因,可以使用postman或多个不同浏览器测试
默认tomcat配置下
同时发送两次"http://localhost:8081/user/asd
"请求,结果如下:
Controller方法被 http-nio-8081-exec-4 开始执行于 2023-10-20 09:29:02
Controller方法被 http-nio-8081-exec-4 结束执行于 2023-10-20 09:29:02
异步方法被 WebmvcThread-3 开始执行于 2023-10-20 09:29:02
Controller方法被 http-nio-8081-exec-3 开始执行于 2023-10-20 09:29:02
Controller方法被 http-nio-8081-exec-3 结束执行于 2023-10-20 09:29:02
异步方法被 WebmvcThread-4 开始执行于 2023-10-20 09:29:02
异步方法被 WebmvcThread-3 结束执行于2023-10-20 09:29:07
Async Result: Demo{one='1', two='2', three='3', four='4'}
异步方法被 WebmvcThread-4 结束执行于2023-10-20 09:29:07
Async Result: Demo{one='1', two='2', three='3', four='4'}
http-nio-端口号-exec-数字
便是tomcat自带线程
两次请求进入,可以看到http-nio-8081-exec-3
与http-nio-8081-exec-4
是同一时间执行的,这是tomcat
默认配置的异步处理。
紧接着能看到,异步方法用我们自己配置的线程池执行了,WebmvcThread-3
与WebmvcThread-4
也是在同一时间执行,异步成功
@Async配置下
同时发送两次"http://localhost:8081/user/asd
"请求,结果如下:
Controller方法被 WebmvcThread-1 开始执行于 2023-10-20 09:45:06
Controller方法被 WebmvcThread-1 结束执行于 2023-10-20 09:45:06
异步方法被 WebmvcThread-2 开始执行于 2023-10-20 09:45:06
Controller方法被 WebmvcThread-3 开始执行于 2023-10-20 09:45:07
Controller方法被 WebmvcThread-3 结束执行于 2023-10-20 09:45:07
异步方法被 WebmvcThread-4 开始执行于 2023-10-20 09:45:07
异步方法被 WebmvcThread-2 结束执行于2023-10-20 09:45:11
Async Result: Demo{one='1', two='2', three='3', four='4'}
异步方法被 WebmvcThread-4 结束执行于2023-10-20 09:45:12
Async Result: Demo{one='1', two='2', three='3', four='4'}