How does SpringBoot program asynchronously? That's how old birds play

Keywords: Java Spring Boot

Hello, I'm misty. Today, we continue to bring you the sixth article of the old bird series of SpringBoot. Let's talk about how to implement asynchronous programming in the SpringBoot project.

Introduction to the old bird series:

1. How does springboot unify the back-end return format? That's how old birds play!

2. How does springboot perform parameter verification? That's how old birds play!

3. How does springboot generate interface documents? Old birds play like this!

4. How does springboot copy objects? Old birds play like this!

5. SpringBoot generates interface documents. I use smart doc

6. How does springboot limit current? That's how old birds play!

First, let's take a look at why asynchronous programming is used in Spring and what problems can it solve?

Why use asynchronous framework? What problems does it solve?

In the daily development of SpringBoot, it is generally called synchronously. However, in practice, there are many scenarios that are very suitable for asynchronous processing, such as registering new users and sending 100 points; Or order successfully, send push message, etc.

Take the use case of registering a new user as an example. Why do you need asynchronous processing?

  • The first reason: fault tolerance and robustness. If there is an exception in sending points, the user registration cannot fail because of sending points;
    Because user registration is the main function and sending points is the secondary function, even if the sending of points is abnormal, the user should be prompted to register successfully, 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 users and 50 milliseconds to send points. If synchronous, it takes 70 milliseconds. If asynchronous, it doesn't need to wait for points, so it takes 20 milliseconds.

Therefore, asynchronous can solve two problems, performance and fault tolerance.

How does SpringBoot implement asynchronous calls?

For asynchronous method calls, the @ Async annotation has been provided since spring 3. We only need to mark this annotation on the method to realize asynchronous calls.

Of course, we also need a configuration class to Enable the asynchronous function through the Enable module driven annotation @ EnableAsync.

Implement asynchronous call

Step 1: create a new configuration class and enable @ Async function 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. Here we choose to use a separate configuration class SyncConfiguration.

@Configuration
@EnableAsync
public class AsyncConfiguration {

}

Step 2: mark the asynchronous call on the method

Add a Component class for business processing, and add @ Async annotation to represent that the method is asynchronous processing.

@Component
@Slf4j
public class AsyncTask {

    @SneakyThrows
    @Async
    public void doTask1() {
        long t1 = System.currentTimeMillis();
        Thread.sleep(2000);
        long t2 = System.currentTimeMillis();
        log.info("task1 cost {} ms" , t2-t1);
    }

    @SneakyThrows
    @Async
    public void doTask2() {
        long t1 = System.currentTimeMillis();
        Thread.sleep(3000);
        long t2 = System.currentTimeMillis();
        log.info("task2 cost {} ms" , t2-t1);
    }
}

Step 3: make asynchronous method calls in the Controller

@RestController
@RequestMapping("/async")
@Slf4j
public class AsyncController {
    @Autowired
    private AsyncTask asyncTask;

    @RequestMapping("/task")
    public void task() throws InterruptedException {
        long t1 = System.currentTimeMillis();
        asyncTask.doTask1();
        asyncTask.doTask2();
        Thread.sleep(1000);
        long t2 = System.currentTimeMillis();
        log.info("main cost {} ms", t2-t1);
    }
}

Through access http://localhost:8080/async/task To view 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

From the log, we can see that the main thread does not need to wait for the asynchronous method to complete, which reduces the response time and improves the interface performance.

Through the above three steps, we can happily use asynchronous methods in SpringBoot to improve the performance of our interface. Is it very simple?

However, if you really write like this in the real project, you will be ruthlessly ridiculed by the old birds. Is that it?

Because the above code ignores the biggest problem, which is to customize the thread pool for the @ Async asynchronous framework.

Why customize the thread pool for @ Async?

Using the @ Async annotation, the SimpleAsyncTaskExecutor thread pool is used by default, which is not a real thread pool.

Thread reuse cannot be realized by using this thread pool. Each call will create a new thread. If threads are constantly created in the system, the system will eventually occupy too much memory and cause OutOfMemoryError error. 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;
  //Judge whether current limiting is enabled. The default value is No
  if (this.isThrottleActive() && startTimeout > 0L) {
    //Perform pre operation to limit current
    this.concurrencyThrottle.beforeAccess();
    this.doExecute(new SimpleAsyncTaskExecutor.ConcurrencyThrottlingRunnable(taskToUse));
  } else {
    //If the flow is not limited, execute the thread task
    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 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;
}

We can also directly observe through the console log above that the thread names printed each time are incremented by [task-1], [task-2], [task-3], [task-4].

Because of this, when using the @ Async asynchronous framework in Spring, we must customize the thread pool to replace the default SimpleAsyncTaskExecutor.

Spring provides a variety of thread pools:

  • SimpleAsyncTaskExecutor: it is not a real thread pool. This class does not reuse threads. Each call will create a new thread.

  • SyncTaskExecutor: this class does not implement asynchronous call, but only a synchronous operation. Only applicable to places that do not require multithreading

  • ConcurrentTaskExecutor: the adapter class of Executor, which is not recommended. Consider using this class only if the ThreadPoolTaskExecutor does not meet the requirements

  • ThreadPoolTaskScheduler: you can use cron expressions

  • ThreadPoolTaskExecutor: most commonly used, recommended. Its essence is the packaging of java.util.concurrent.ThreadPoolExecutor

Implement a custom thread pool for @ Async

@Configuration
@EnableAsync
public class SyncConfiguration {
    @Bean(name = "asyncPoolTaskExecutor")
    public ThreadPoolTaskExecutor executor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        //Number of core threads
        taskExecutor.setCorePoolSize(10);
        //The thread pool maintains the maximum number of threads. Threads exceeding the number of core threads will be applied only after the buffer queue is full
        taskExecutor.setMaxPoolSize(100);
        //Cache queue
        taskExecutor.setQueueCapacity(50);
        //Allowed idle time. When the idle time exceeds that of the core thread, the thread other than the core thread will be destroyed after the idle time arrives
        taskExecutor.setKeepAliveSeconds(200);
        //Asynchronous method internal thread name
        taskExecutor.setThreadNamePrefix("async-");
        /**
         * When the task cache queue of the thread pool is full and the number of threads in the thread pool reaches maximumPoolSize, the task rejection policy will be adopted if there are still tasks coming
         * There are usually four strategies:
         * ThreadPoolExecutor.AbortPolicy:Discard the task and throw a RejectedExecutionException exception.
         * ThreadPoolExecutor.DiscardPolicy: It also discards the task without throwing an exception.
         * ThreadPoolExecutor.DiscardOldestPolicy: Discard the task at the top of the queue, and then try to execute the task again (repeat the process)
         * ThreadPoolExecutor.CallerRunsPolicy: Retry adding the current task, and automatically call the execute() method repeatedly until it succeeds
         */
        taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        taskExecutor.initialize();
        return taskExecutor;
    }
}

After customizing the thread pool, we can boldly use the asynchronous processing capability provided by @ Async.

Multiple thread pool processing

In real Internet project development, for high concurrency requests, the general practice is to isolate the high concurrency interface from a separate thread pool.

Suppose there are two high concurrency interfaces: one is to modify the user information interface and refresh the user redis cache; One is the order placement interface, which sends app push information. Two thread pools are often defined according to the interface characteristics. In this case, we need to distinguish by specifying the thread pool name when using @ Async.

Specify the thread pool name for @ Async

@SneakyThrows
@Async("asyncPoolTaskExecutor")
public void doTask1() {
  long t1 = System.currentTimeMillis();
  Thread.sleep(2000);
  long t2 = System.currentTimeMillis();
  log.info("task1 cost {} ms" , t2-t1);
}

When there are multiple thread pools in the system, we can also configure a default thread pool. For non default asynchronous tasks, we can specify the thread pool name through @ Async("otherTaskExecutor").

Configure default thread pool

You can modify the configuration class to implement AsyncConfigurer, override the getAsyncExecutor() method, and specify the default thread pool:

@Configuration
@EnableAsync
@Slf4j
public class AsyncConfiguration implements AsyncConfigurer {

    @Bean(name = "asyncPoolTaskExecutor")
    public ThreadPoolTaskExecutor executor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        //Number of core threads
        taskExecutor.setCorePoolSize(2);
        //The thread pool maintains the maximum number of threads. Threads exceeding the number of core threads will be applied only after the buffer queue is full
        taskExecutor.setMaxPoolSize(10);
        //Cache queue
        taskExecutor.setQueueCapacity(50);
        //Allowed idle time. When the idle time exceeds that of the core thread, the thread other than the core thread will be destroyed after the idle time arrives
        taskExecutor.setKeepAliveSeconds(200);
        //Asynchronous method internal thread name
        taskExecutor.setThreadNamePrefix("async-");
        /**
         * When the task cache queue of the thread pool is full and the number of threads in the thread pool reaches maximumPoolSize, the task rejection policy will be adopted if there are still tasks coming
         * There are usually four strategies:
         * ThreadPoolExecutor.AbortPolicy:Discard the task and throw a RejectedExecutionException exception.
         * ThreadPoolExecutor.DiscardPolicy: It also discards the task without throwing an exception.
         * ThreadPoolExecutor.DiscardOldestPolicy: Discard the task at the top of the queue, and then try to execute the task again (repeat the process)
         * ThreadPoolExecutor.CallerRunsPolicy: Retry adding the current task, and automatically call the execute() method repeatedly until it succeeds
         */
        taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        taskExecutor.initialize();
        return taskExecutor;
    }

    /**
     * Specifies the default thread pool
     */
    @Override
    public Executor getAsyncExecutor() {
        return executor();
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return (ex, method, params) ->
            log.error("Thread pool execution task sending unknown error,Execution method:{}",method.getName(),ex);
    }
}

As follows, doTask1() uses the default thread pool asyncPoolTaskExecutor, and doTask2() uses the thread pool otherTaskExecutor, which is very flexible.

@Async
public void doTask1() {
  long t1 = System.currentTimeMillis();
  Thread.sleep(2000);
  long t2 = System.currentTimeMillis();
  log.info("task1 cost {} ms" , t2-t1);
}

@SneakyThrows
@Async("otherTaskExecutor")
public void doTask2() {
  long t1 = System.currentTimeMillis();
  Thread.sleep(3000);
  long t2 = System.currentTimeMillis();
  log.info("task2 cost {} ms" , t2-t1);
}

Summary

@Async asynchronous method is often used in daily development. We should master it well and strive to become an old bird as soon as possible!!!

tips: the source of the old bird series has been uploaded to GitHub. The need to click the bottom card concerns the official account and answers the keyword 0923 to get the source address.

Posted by the mysox1 on Tue, 30 Nov 2021 18:45:55 -0800