前言
本文将探讨在SpringBoot中的线程问题。Controller是线程安全的吗?如果我们想在用户请求时,开辟新的异步任务,该如何操作?
原文地址:https://xuedongyun.cn/post/59240/
Controller线程安全
首先我们先测试一下Controller
的线程问题。我们在Controller
中创建成员变量,并在请求中对它进行更改(请注意,这是非常规操作,请勿在开发中使用)。并且,我们在收到请求时,打印当前线程的Id。
@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
的线程池中拿到一个线程进行使用。
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
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@RestController
public class HelloController {
}
此时可以看到,每次请求都会创建一个新的Controller
实例,所以其实是线程安全的。
i = 1
threadId = 40
i = 1
threadId = 43
i = 1
threadId = 42
i = 1
threadId = 40
无论如何,请尽量不要在
Controller
中使用成员变量
@Async异步调用
假设用户提交一个任务,后端需要处理很久,最佳的方案应该是使用异步调用。用户提交任务之后,后端开辟新的线程处理任务。
首先在需要异步执行的方法上加上@Async
注解
@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
注解,开启异步功能
@EnableAsync
@SpringBootApplication
public class SpringBootSourceApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringBootSourceApplication.class, args);
}
}
现在我们就能对用户的请求进行异步的处理了,用户发起请求能直接收到响应,3000ms后服务器才完成任务
@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默认线程池的参数
task:
execution:
pool:
core-size: 5
max-size: 50
queue-capacity: 200
thread-name-prefix: myTask-
// 代码有删改,具体配置类
@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
,默认线程池已经没了)
@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接口
在配置类中实现AsyncConfigurer
接口
@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属性,为不同任务指定不同的线程池
// 不同方法,指定不同的线程池
@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());
}
@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原理