Spring asynchronous processing – @Async annotation

1. Introduction

@Async is a very useful asynchronous writing method in the spring framework:

1. Use the @Async annotation on the method to declare that the method is an asynchronous task;

2. Use the @Async annotation on the class to declare that all methods in the class are asynchronous tasks;

3. The class object using the method of this annotation must be a bean object managed by spring;

4. To use asynchronous tasks, you need to enable asynchronous configuration on the main class, that is, configure the @EnableAsync annotation;

How the @Async annotation works
When the annotated method is called, Spring will transfer the execution of the method to a thread in the thread pool for processing. After execution is completed, the return value of the method will be encapsulated by Future or CompletableFuture in order to obtain the return result of the method.

The @Async annotation is suitable for the following scenarios and has the following advantages:

  • Network requests: When processing network requests, you can use the @Async annotation to separate request sending and response processing to improve the system’s concurrent processing capabilities.
  • Time-consuming calculations: For calculation tasks that require a lot of time, you can use the @Async annotation to execute the calculation process in the background to avoid blocking the main thread and improve the system’s response speed.
  • Parallel processing: Through the @Async annotation, multiple tasks can be executed at the same time and multiple independent tasks can be processed in parallel, thereby reducing the overall processing time.
  • Improved responsiveness: Using asynchronous programming can avoid blocking the main thread, improve the concurrency and responsiveness of the system, and enhance user experience.
  • Code simplification: Using the @Async annotation can simplify the programming model and separate asynchronous execution logic from business logic, making the code clearer and easier to maintain.

Asynchronous execution can make full use of system resources and improve the system’s throughput and concurrent processing capabilities by decomposing tasks into multiple concurrently executed subtasks, thereby improving system performance and responsiveness. The @Async annotation simplifies the implementation of asynchronous programming and enables developers to use the asynchronous processing mechanism more conveniently. At the same time, it can also make the code easier to read and maintain, improving development efficiency.

2. Understanding synchronization and asynchronousness

1. **Synchronization: **Synchronization means that the entire processing process is executed sequentially. When each process is completed, the results are returned.
2. Asynchronous: Asynchronous calling only sends the calling instructions. The caller does not need to wait for the called method to be completely executed; instead, it continues to execute the following process.
**For example:** In a certain call, three process methods A, B, and C need to be called sequentially; if they are all synchronous calls, they need to be executed sequentially before the process is counted as completed; such as B is an asynchronous calling method. After executing A, B is called. It does not wait for B to complete, but starts executing and calls C. After C is executed, it means that the process is completed. In Java, when dealing with similar scenarios, it is generally based on creating independent threads to complete the corresponding asynchronous calling logic. Through the execution process between the main thread and different business sub-threads, after starting the independent thread , the main thread continues execution without stalling and waiting.

1. Synchronous call

Let’s use a simple example to intuitively understand what synchronous calls are:
1. Define the Task class and create three processing functions to simulate the operations of three tasks respectively. The operation time is randomly selected (within 10 seconds)

@Component
public class Task {<!-- -->
 
    public static Random random =new Random();
 
    public void doTaskOne() throws Exception {<!-- -->
        System.out.println("Start task one");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        System.out.println("Complete task 1, time taken:" + (end - start) + "milliseconds");
    }
 
    public void doTaskTwo() throws Exception {<!-- -->
        System.out.println("Start task 2");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        System.out.println("Complete task two, time taken:" + (end - start) + "milliseconds");
    }
 
    public void doTaskThree() throws Exception {<!-- -->
        System.out.println("Start task three");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        System.out.println("Complete task three, time taken:" + (end - start) + "milliseconds");
    }
 
}

2. In the unit test case, inject the Task object and execute the three functions doTaskOne, doTaskTwo, and doTaskThree in the test case.

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
public class ApplicationTests {<!-- -->
 
    @Autowired
    private Task task;
 
    @Test
    public void test() throws Exception {<!-- -->
        task.doTaskOne();
        task.doTaskTwo();
        task.doTaskThree();
    }
 
}

3. Execute the unit test and you will see output similar to the following:
Start task one
Complete task 1, time taken: 4256 milliseconds

Start task two
Complete task 2, time taken: 4957 milliseconds

Start task three
Complete task three, time taken: 7173 milliseconds

Task one, task two, and task three are executed sequentially. In other words, the three functions doTaskOne, doTaskTwo, and doTaskThree are executed sequentially.

2. Asynchronous call

Although the above synchronous call successfully executed the three tasks, it can be seen that the execution time is relatively long. If there is no dependency between the three tasks themselves and they can be executed concurrently, the execution efficiency of the synchronous call will be relatively poor. You can consider using asynchronous calls to execute concurrently.
In Spring Boot, we can simply change the original synchronous function into an asynchronous function by using the **@Async annotation**, and the Task class is changed to the following mode:

 public static Random random = new Random();
 
    @Async
    public Future<String> doTaskOne() throws Exception {<!-- -->
        System.out.println("Start task 1");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        System.out.println("Complete task 1, time taken:" + (end - start) + "milliseconds");
        return new AsyncResult<>("Task 1 completed");
    }
 
    @Async
    public Future<String> doTaskTwo() throws Exception {<!-- -->
        System.out.println("Start task 2");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        System.out.println("Complete task 2, time taken:" + (end - start) + "milliseconds");
        return new AsyncResult<>("Task 2 completed");
    }
 
    @Async
    public Future<String> doTaskThree() throws Exception {<!-- -->
        System.out.println("Start task 3");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        System.out.println("Complete task 3, time taken:" + (end - start) + "milliseconds");
        return new AsyncResult<>("Task 3 completed");
    }

See what we’ve changed:

1. Record the start time at the beginning of the test case
2. When calling three asynchronous functions, return a Future type result object
3. After calling the three asynchronous functions, start a loop and judge whether the three asynchronous functions have ended based on the returned Future object. If they are all over, end the cycle; if not, wait 1 second before making a decision.

Start task one
Start task two
Start task three
Complete task three, time taken: 37 milliseconds
Complete task 2, time taken: 3661 milliseconds
Complete task 1, time taken: 7149 milliseconds
All tasks are completed, total time taken: 8025 milliseconds

It can be seen that through asynchronous calling, tasks one, two and three are executed concurrently, which effectively reduces the total running time of the program

3. Use of @Async

1. When using the @Async annotation, you must first add the @EnableAsync annotation to the startup class.

// Enabling method based on Java configuration:
@Configuration
@EnableAsync
public class SpringAsyncConfig {<!-- --> ... }
 
// Spring boot enabled:
@EnableAsync
@EnableTransactionManagement
public class SettlementApplication {<!-- -->
    public static void main(String[] args) {<!-- -->
        SpringApplication.run(SettlementApplication.class, args);
    }
}

2. Add @Async annotation
1) Calling method without return value

/**
    * Asynchronous call with parameters Asynchronous methods can pass in parameters
    * If the return value is void, the exception will be handled by AsyncUncaughtExceptionHandler
    * @param s
    */
   @Async
   public void asyncInvokeWithException(String s) {<!-- -->
       log.info("asyncInvokeWithParameter, parementer={}", s);
       throw new IllegalArgumentException(s);
   }

2) Future return value

 @Async("MyExecutor")
public Future<Map<Long, List>> queryMap(List ids) {<!-- -->
    List<> result = businessService.queryMap(ids);
    ............
    Map<Long, List> resultMap = Maps.newHashMap();
    ...
    return new AsyncResult<>(resultMap);
}

4. Thread pool issue of @Async

1. Spring default thread pool

1. When the @Async annotation is used, if the name of the thread pool is not specified, the spring default thread pool is used. The spring default thread pool is SimpleAsyncTaskExecutor
2. Once the @Async annotation is marked on the method, when other threads call this method, a new sub-thread will be started to process the business logic asynchronously.

spring’s default thread pool configuration is
 
    Default number of core threads: 8
    
    Maximum number of threads: Integet.MAX_VALUE
 
    Queue uses LinkedBlockingQueue
 
    The capacity is: Integet.MAX_VALUE
 
    Idle thread retention time: 60s
 
    Thread pool rejection policy: AbortPolicy

As you can see, it has no limit on the maximum number of threads, that is, unlimited number of threads will be created.

2. Custom thread pool

1. Write configuration file

spring:
  task:
    execution:
      pool:
        max-size: 6
        core-size: 3
        keep-alive: 3s
        queue-capacity: 1000
        thread-name-prefix: name

2. Write configuration class

@Configuration
@Data
public class ExecutorConfig{<!-- -->
    /**
     * Core thread
     */
    private int corePoolSize;
    /**
     * Maximum threads
     */
    private int maxPoolSize;
    /**
     * Queue capacity
     */
    private int queueCapacity;
    /**
     *Hold time
     */
    private int keepAliveSeconds;
    /**
     * name prefix
     */
    private String preFix;
 
    @Bean("MyExecutor")
    public Executor myExecutor() {<!-- -->
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(corePoolSize);
        executor.setMaxPoolSize(maxPoolSize);
        executor.setQueueCapacity(queueCapacity);
        executor.setKeepAliveSeconds(keepAliveSeconds);
        executor.setThreadNamePrefix(preFix);
        executor.setRejectedExecutionHandler( new ThreadPoolExecutor.AbortPolicy());
        executor.initialize();
        return executor;
    }
}

3. Use @Async annotation and specify a custom thread pool

 @Component
public class MyAsyncTask {<!-- -->
     @Async("MyExecutor") //Use a custom thread pool (executor)
    public void asyncCpsItemImportTask(Long platformId, String jsonList){<!-- -->
        //...specific business logic
    }
}

5. Transaction and exception handling of @Async

1. Since the @Async annotation is executed asynchronously, when it operates the database, the outer transaction control will not be able to manage the transaction. The @Transactional annotation can be placed on internal methods that require transactions to achieve transaction management.

2. Also due to the asynchronous relationship, the exception thrown in the @Async method cannot be caught by the external try…catch, so the try…catch must be written into the asynchronous method