前言
本文将探讨在SpringBoot中的线程问题。Controller是线程安全的吗?如果我们想在用户请求时,开辟新的异步任务,该如何操作?
原文地址:https://xuedongyun.cn/post/59240/
Controller线程安全
首先我们先测试一下Controller
的线程问题。我们在Controller
中创建成员变量,并在请求中对它进行更改(请注意,这是非常规操作,请勿在开发中使用)。并且,我们在收到请求时,打印当前线程的Id。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @RestController public class HelloController { private int i = 0;
@PostMapping("/hello") String hello() { i = i + 1; System.out.println("i = " + i);
long threadId = Thread.currentThread().getId(); System.out.println("threadId = " + threadId);
return "hello"; } }
|
最终结果显示,Controller
默认是单例模式,而这种模式下是线程不安全的。我们每次的请求,都会从SpringBoot
的线程池中拿到一个线程进行使用。
1 2 3 4 5 6 7 8
| i = 1 threadId = 40 i = 2 threadId = 43 i = 3 threadId = 42 i = 4 threadId = 40 // 线程池中,线程可以复用
|
如果我们将Controller
指定为单例模式,又会如何呢?我们使用@Scope
注解,指定HelloController
为原型模式。
@Scope
有五种作用域:
- SINGLETON:单例模式,默认模式,不写的时候默认是SINGLETON
- PROTOTYPE:原型模式
- REQUEST:同一次请求则只创建一次实例
- SESSION:同一个session只创建一次实例
- GLOBAL SESSION:全局的web域,类似于servlet中的application
1 2 3 4 5
| @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) @RestController public class HelloController { }
|
此时可以看到,每次请求都会创建一个新的Controller
实例,所以其实是线程安全的。
1 2 3 4 5 6 7 8
| i = 1 threadId = 40 i = 1 threadId = 43 i = 1 threadId = 42 i = 1 threadId = 40
|
无论如何,请尽量不要在Controller
中使用成员变量
@Async异步调用
假设用户提交一个任务,后端需要处理很久,最佳的方案应该是使用异步调用。用户提交任务之后,后端开辟新的线程处理任务。
首先在需要异步执行的方法上加上@Async
注解
1 2 3 4 5 6 7 8 9 10 11
| @Component @Slf4j public class AsyncTask {
@Async public void doTask(String taskName) throws InterruptedException { Thread.sleep(3000); log.info(Thread.currentThread().getName()); log.info("task: " + taskName+ " is finished!"); } }
|
然后需要在主启动类上加上@EnableAsync
注解,开启异步功能
1 2 3 4 5 6 7 8 9
| @EnableAsync @SpringBootApplication public class SpringBootSourceApplication {
public static void main(String[] args) { ConfigurableApplicationContext context = SpringApplication.run(SpringBootSourceApplication.class, args);
} }
|
现在我们就能对用户的请求进行异步的处理了,用户发起请求能直接收到响应,3000ms后服务器才完成任务
1 2 3 4 5 6 7 8 9 10 11 12
| @RestController public class AsyncController {
@Resource private AsyncTask asyncTask;
@GetMapping("/task") private String task(String taskName) throws InterruptedException { asyncTask.doTask(taskName); return "success to submit"; } }
|
自定义线程池
配置文件修改默认线程池
我们可以通过配置文件来修改SpringBoot默认线程池的参数
1 2 3 4 5 6 7
| task: execution: pool: core-size: 5 max-size: 50 queue-capacity: 200 thread-name-prefix: myTask-
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @ConfigurationProperties("spring.task.execution") public class TaskExecutionProperties {
private final Pool pool = new Pool(); private final Shutdown shutdown = new Shutdown(); private String threadNamePrefix = "task-";
public static class Pool { private int queueCapacity = Integer.MAX_VALUE; private int coreSize = 8; private int maxSize = Integer.MAX_VALUE; } }
|
配置类定义新的线程池
也可以在配置类中定义自己的线程池(由于@ConditionalOnMissingBean
,默认线程池已经没了)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Configuration public class AsyncConfig {
@Bean(name = "customTaskExecutor") public ThreadPoolTaskExecutor customTaskExecutor() { ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); taskExecutor.setCorePoolSize(2); taskExecutor.setMaxPoolSize(10); taskExecutor.setKeepAliveSeconds(200); taskExecutor.setThreadNamePrefix(executorPrefix); taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); taskExecutor.initialize(); return taskExecutor; } }
|
在配置类中实现AsyncConfigurer
接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| @Configuration public class AsyncConfig implements AsyncConfigurer{
@Override public Executor getAsyncExecutor() { return getExecutor(); }
@Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return AsyncConfigurer.super.getAsyncUncaughtExceptionHandler(); }
public ThreadPoolTaskExecutor getExecutor() { ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); taskExecutor.setCorePoolSize(2); taskExecutor.setMaxPoolSize(10); taskExecutor.setKeepAliveSeconds(200); taskExecutor.setThreadNamePrefix("custom-"); taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); taskExecutor.initialize(); return taskExecutor; } }
|
为任务指定不同线程池
若@Async
不指定具体的线程池,会使用默认的线程池。具体规则如下:
若容器中只有一个TaskExecutor
组件,其为默认执行器;
若不唯一,拿名字叫”taskExecutor”的,类型为Executor
的组件。
若都不满足,使用SimpleAsyncTaskExecutor
作为默认执行器(每次执行被注解方法时,单独创建一个Thread来执行)
我们可以通过value属性,为不同任务指定不同的线程池
1 2 3 4 5 6 7 8 9 10 11 12 13
|
@Async("otherTaskExecutor") public void doTask1() throws InterruptedException { Thread.sleep(3000); log.info(Thread.currentThread().getName()); }
@Async("testTaskExecutor") public void doTask2() throws InterruptedException { Thread.sleep(3000); log.info(Thread.currentThread().getName()); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @Bean(name = "otherTaskExecutor") public ThreadPoolTaskExecutor otherExecutor() { ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); taskExecutor.setCorePoolSize(2); taskExecutor.setMaxPoolSize(10); taskExecutor.setKeepAliveSeconds(200); taskExecutor.setThreadNamePrefix("other-"); taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); taskExecutor.initialize(); return taskExecutor; }
@Bean(name = "testTaskExecutor") public ThreadPoolTaskExecutor testExecutor() { ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); taskExecutor.setCorePoolSize(2); taskExecutor.setMaxPoolSize(10); taskExecutor.setKeepAliveSeconds(200); taskExecutor.setThreadNamePrefix("test-"); taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); taskExecutor.initialize(); return taskExecutor; }
|
原理
如果你想知道背后的原理(源码),可以查看我之前的文章:SpringBoot源码系列(10):@Async原理