Hello, I am Misty. In the sixth installment of the SpringBoot series, we will talk about how to implement asynchronous programming in SpringBoot projects.
Introduction to the old Bird series:
1. How does SpringBoot unify the backend return format? This is how old birds play!
2. How to verify SpringBoot parameters? That’s how old birds play!
3. How does SpringBoot generate interface documents?
4. SpringBoot how to replicate objects, old birds are playing this way!
5. SpringBoot generates interface documentation. I use smart-doc
6. How does SpringBoot implement traffic limiting? That’s what old birds do!
Let’s start by looking at why asynchronous programming is used in Spring and what problems it solves.
Why use an asynchronous framework, and what problems does it solve?
In the daily development of SpringBoot, it is usually called synchronously. But in practice, there are many scenarios that are very suitable for asynchronous processing, such as: registering a new user, giving 100 credits; Or successful order, send push message and so on.
Take the use case of registering a new user. Why do you do it asynchronously?
- The first reason is fault tolerance and robustness. If there is an anomaly in sending points, users cannot fail to register because of sending points. Because user registration is the main function, and sending points is the secondary function, even if the points are abnormal, the user should be reminded of the success of registration, and then compensation will be made for the abnormal points.
- The second reason is to improve performance. For example, it takes 20 milliseconds to register a user and 50 milliseconds to send credits. If you use synchronization, the total time is 70 milliseconds.
Therefore, asynchrony can solve two problems, performance and fault tolerance.
How does SpringBoot implement asynchronous calls?
For asynchronous method calls, the @async annotation is provided starting with Spring3, and we just need to annotate the annotation on the method to implement the asynchronous call.
Of course, we also need a configuration class to Enable async via the Enable module driver annotation @enableAsync.
Implementing asynchronous calls
Step 1: Create a configuration class and enable @Async support
Use @enableAsync to enable asynchronous task support. The @EnableAsync annotation can be placed directly on the SpringBoot startup class or separately on other configuration classes. We chose to use a separate configuration class, SyncConfiguration, here.
@Configuration
@EnableAsync
public class AsyncConfiguration {
}
Copy the code
Step 2: Mark the asynchronous call on the method
Add a Component class for business processing and an @async annotation to indicate that the method is asynchronous processing.
@Component
@Slf4j
public class AsyncTask {
@SneakyThrows
@Async
public void doTask1(a) {
long t1 = System.currentTimeMillis();
Thread.sleep(2000);
long t2 = System.currentTimeMillis();
log.info("task1 cost {} ms" , t2-t1);
}
@SneakyThrows
@Async
public void doTask2(a) {
long t1 = System.currentTimeMillis();
Thread.sleep(3000);
long t2 = System.currentTimeMillis();
log.info("task2 cost {} ms", t2-t1); }}Copy the code
Step 3: Make an asynchronous method call in the Controller
@RestController
@RequestMapping("/async")
@Slf4j
public class AsyncController {
@Autowired
private AsyncTask asyncTask;
@RequestMapping("/task")
public void task(a) throws InterruptedException {
long t1 = System.currentTimeMillis();
asyncTask.doTask1();
asyncTask.doTask2();
Thread.sleep(1000);
long t2 = System.currentTimeMillis();
log.info("main cost {} ms", t2-t1); }}Copy the code
By visiting http://localhost:8080/async/task to check the console log:
2021-11-25 15:48:37 [http-nio-8080-exec-8] INFO com.jianzh5.blog.async.AsyncController:26 - main cost 1009 ms
2021-11-25 15:48:38 [task-1] INFO com.jianzh5.blog.async.AsyncTask:22 - task1 cost 2005 ms
2021-11-25 15:48:39 [task-2] INFO com.jianzh5.blog.async.AsyncTask:31 - task2 cost 3005 ms
Copy the code
As you can see from the logs, the main thread does not need to wait for the asynchronous method to complete, reducing the response time and improving interface performance.
With the above three steps we can joyfully use asynchronous methods in SpringBoot to improve our interface performance, isn’t it easy?
But if you did write that in a real project, you’d be ridiculed by the old-timers. Is that it?
Because the code above ignores one of the biggest problems, which is customizing thread pools for the @Async asynchronous framework.
Why custom thread pool for @Async?
With the @async annotation, by default the SimpleAsyncTaskExecutor thread pool is used, which is not really a thread pool.
Thread reuse cannot be achieved with this thread pool and a new thread is created for each invocation. If threads are constantly created in the system, the system memory usage is too high and OutOfMemoryError is raised. The key codes are as follows:
public void execute(Runnable task, long startTimeout) {
Assert.notNull(task, "Runnable must not be null");
Runnable taskToUse = this.taskDecorator ! =null ? this.taskDecorator.decorate(task) : task;
// Determine whether to enable traffic limiting. The default value is no
if (this.isThrottleActive() && startTimeout > 0L) {
// Perform the preceding operation to limit the current
this.concurrencyThrottle.beforeAccess();
this.doExecute(new SimpleAsyncTaskExecutor.ConcurrencyThrottlingRunnable(taskToUse));
} else {
// Execute thread tasks in the case of unrestricted flow
this.doExecute(taskToUse); }}protected void doExecute(Runnable task) {
// keep creating threads
Thread thread = this.threadFactory ! =null ? this.threadFactory.newThread(task) : this.createThread(task);
thread.start();
}
// Create a thread
public Thread createThread(Runnable runnable) {
// Specify thread name, task-1, task-2...
Thread thread = new Thread(this.getThreadGroup(), runnable, this.nextThreadName());
thread.setPriority(this.getThreadPriority());
thread.setDaemon(this.isDaemon());
return thread;
}
Copy the code
The thread name is [task-1], [task-2], [task-3], [task-4]….. The increment.
This is why we made sure to customize the thread pool when using Spring’s @Async asynchronous framework instead of the default SimpleAsyncTaskExecutor.
Spring provides a variety of thread pools:
SimpleAsyncTaskExecutor: Not a real thread pool. This class does not reuse threads and creates a new thread each time it is called.
SyncTaskExecutor: This class does not implement an asynchronous call, just a synchronous operation. Only for places that do not require multithreading
ConcurrentTaskExecutor: Executor adaptation class. This class is not recommended. Consider using this class only if ThreadPoolTaskExecutor does not meet the requirements
ThreadPoolTaskScheduler: Cron expressions can be used
ThreadPoolTaskExecutor: Most commonly used, recommended. The essence of which is the Java. Util. Concurrent. ThreadPoolExecutor packaging
Implement a custom thread pool for @async
@Configuration
@EnableAsync
public class SyncConfiguration {
@Bean(name = "asyncPoolTaskExecutor")
public ThreadPoolTaskExecutor executor(a) {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
// Number of core threads
taskExecutor.setCorePoolSize(10);
// The thread pool maintains a maximum number of threads, and only requests more threads than the core thread count after the buffer queue is full
taskExecutor.setMaxPoolSize(100);
// Cache the queue
taskExecutor.setQueueCapacity(50);
Threads that exceed the core thread will be destroyed when the idle time expires
taskExecutor.setKeepAliveSeconds(200);
// The name of the thread inside the asynchronous method
taskExecutor.setThreadNamePrefix("async-");
/** * When the task cache queue in the thread pool is full and the number of threads in the thread pool reaches maximumPoolSize, the task rejection policy is adopted if additional tasks arrive. * ThreadPoolExecutor. AbortPolicy: discard task and throw RejectedExecutionException anomalies. * ThreadPoolExecutor DiscardPolicy: discard task too, but does not throw an exception. * ThreadPoolExecutor. DiscardOldestPolicy: discard queue in front of the task, and then to try to perform a task (repeat) * ThreadPoolExecutor CallerRunsPolicy: Retry adding the current task, automatically repeating the execute() method until it succeeds */
taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
taskExecutor.initialize();
returntaskExecutor; }}Copy the code
With a custom thread pool, we can make bold use of the asynchronous processing power provided by @async.
Multiple thread pool processing
In the real Internet project development, the common practice is to deal with the request of high concurrency interface separately from the thread pool.
Assume that there are two high concurrent interfaces: one is to modify user information interface and refresh user redis cache; One is the order placing interface, which sends app push information. It is common to define two thread pools based on interface characteristics, and in this case we need to specify the thread pool name when using @async.
Specify the thread pool name for @async
@SneakyThrows
@Async("asyncPoolTaskExecutor")
public void doTask1(a) {
long t1 = System.currentTimeMillis();
Thread.sleep(2000);
long t2 = System.currentTimeMillis();
log.info("task1 cost {} ms" , t2-t1);
}
Copy the code
If the system has multiple thread pools, you can also configure a default thread pool and specify the pool name with @async (“otherTaskExecutor”) for non-default asynchronous tasks.
Configure the default thread pool
You can modify the configuration class to implement AsyncConfigurer and override the getAsyncExecutor() method to specify the default thread pool:
@Configuration
@EnableAsync
@Slf4j
public class AsyncConfiguration implements AsyncConfigurer {
@Bean(name = "asyncPoolTaskExecutor")
public ThreadPoolTaskExecutor executor(a) {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
// Number of core threads
taskExecutor.setCorePoolSize(2);
// The thread pool maintains a maximum number of threads, and only requests more threads than the core thread count after the buffer queue is full
taskExecutor.setMaxPoolSize(10);
// Cache the queue
taskExecutor.setQueueCapacity(50);
Threads that exceed the core thread will be destroyed when the idle time expires
taskExecutor.setKeepAliveSeconds(200);
// The name of the thread inside the asynchronous method
taskExecutor.setThreadNamePrefix("async-");
/** * When the task cache queue in the thread pool is full and the number of threads in the thread pool reaches maximumPoolSize, the task rejection policy is adopted if additional tasks arrive. * ThreadPoolExecutor. AbortPolicy: discard task and throw RejectedExecutionException anomalies. * ThreadPoolExecutor DiscardPolicy: discard task too, but does not throw an exception. * ThreadPoolExecutor. DiscardOldestPolicy: discard queue in front of the task, and then to try to perform a task (repeat) * ThreadPoolExecutor CallerRunsPolicy: Retry adding the current task, automatically repeating the execute() method until it succeeds */
taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
taskExecutor.initialize();
return taskExecutor;
}
/** * specifies the default thread pool */
@Override
public Executor getAsyncExecutor(a) {
return executor();
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler(a) {
return (ex, method, params) ->
log.error("Thread pool execute task send unknown error, execute method: {}",method.getName(),ex); }}Copy the code
Here, the doTask1() method uses the thread pool asyncPoolTaskExecutor by default, and doTask2() uses the thread pool otherTaskExecutor, which is very flexible.
@Async
public void doTask1(a) {
long t1 = System.currentTimeMillis();
Thread.sleep(2000);
long t2 = System.currentTimeMillis();
log.info("task1 cost {} ms" , t2-t1);
}
@SneakyThrows
@Async("otherTaskExecutor")
public void doTask2(a) {
long t1 = System.currentTimeMillis();
Thread.sleep(3000);
long t2 = System.currentTimeMillis();
log.info("task2 cost {} ms" , t2-t1);
}
Copy the code
summary
@async asynchronous method is often used in daily development, we have a good grasp of it, and strive to become an old bird as soon as possible!!
Tips: Old bird series source code has been uploaded to GitHub, need to pay attention to this public account JAVA daily record and reply to the keyword 0923 to obtain the source address.