SpringBoot implements asynchronous call | @ Async

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 AsyncConfiguration.

As for why to use thread pool, we will talk about it later.

import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * Description: asynchronous configuration
 * Created by: HuangTuL
 */
@Slf4j
@Configuration
@EnableAsync    // It can be placed on the startup class or a separate configuration class
public class AsyncConfiguration implements AsyncConfigurer {

    @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);
        //Set the idle time of the thread. When the idle time exceeds that of the core thread, the thread will be destroyed after it reaches the idle time
        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
     * The {@link Executor} instance to be used when processing async method invocations.
     */
    @Override
    public Executor getAsyncExecutor() {
        return executor();
    }

    /**
     * The {@link AsyncUncaughtExceptionHandler} instance to be used
     * when an exception is thrown during an asynchronous method execution
     * with {@code void} return type.
     */
    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return (ex, method, params) -> log.error("Thread pool execution task sending unknown error, Execution method:{}", method.getName(), ex);
    }
}

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.

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

/**
 * Description: asynchronous method call
 * Created by: HuangTuL
 */
@Slf4j
@Component
public class AsyncTask {

    @SneakyThrows
    @Async  // If the default thread pool is set in the asynchronous configuration class, you do not need to specify the thread pool name
//    @Async("asyncPoolTaskExecutor")
    public void doTask1() {
        long t1 = System.currentTimeMillis();
        Thread.sleep(2000);
        long t2 = System.currentTimeMillis();
        log.info("task1 Method time consuming {} ms" , t2-t1);
    }

    @SneakyThrows
    @Async
//    @Async("otherPoolTaskExecutor") / / name of other thread pools
    public void doTask2() {
        long t1 = System.currentTimeMillis();
        Thread.sleep(3000);
        long t2 = System.currentTimeMillis();
        log.info("task2 Method time consuming {} ms" , t2-t1);
    }
}

Step 3: make asynchronous method calls in the Controller

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping("/async")
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 Method time consuming{} ms", t2-t1);
    }
}

Through access http://localhost:8080/async/task To view the console log:

[2021-12-01 20:21:36.036] [INFO] [http-nio-8080-exec-1] - task(AsyncController.java:27) - main The method takes 1009 hours ms
[2021-12-01 20:21:37.037] [INFO] [async-1] - doTask1(AsyncTask.java:23) - task1 Method time consuming 2004 ms
[2021-12-01 20:21:38.038] [INFO] [async-2] - doTask2(AsyncTask.java:32) - task2 The method takes 3003 hours ms

It can be seen from the log 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 use asynchronous methods in SpringBoot to improve the performance of our interface.

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

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.ThreadPoolExecutor;

/**
 * Description: asynchronous configuration 2
 * Created by: HuangTuL
 */
@Configuration
@EnableAsync    // It can be placed on the startup class or a separate configuration class
public class AsyncConfiguration2 {
    @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 configuring the custom 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 Method time consuming {} 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:

import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * Description: asynchronous configuration
 * Created by: HuangTuL
 */
@Slf4j
@Configuration
@EnableAsync    // It can be placed on the startup class or a separate configuration class
public class AsyncConfiguration implements AsyncConfigurer {

    @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);
        //Set the idle time of the thread. When the idle time exceeds that of the core thread, the thread will be destroyed after it reaches the idle time
        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
     * The {@link Executor} instance to be used when processing async method invocations.
     */
    @Override
    public Executor getAsyncExecutor() {
        return executor();
    }

    /**
     * The {@link AsyncUncaughtExceptionHandler} instance to be used
     * when an exception is thrown during an asynchronous method execution
     * with {@code void} return type.
     */
    @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.

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

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

END

Tags: Java thread pool async Task

Posted on Wed, 01 Dec 2021 23:04:31 -0500 by alexsaidani