Preface
Recently, I need to use multi-threading, it is very difficult to maintain thread pool by myself. I happen to see an example of springboot integrated thread pool. Here I try and summarize it, record it, and share it with friends I need.
This multithreaded implementation is relatively simple, regardless of transactions. There are the following main points:
Turn on asynchronous execution support by adding the @EnableAsync annotation to the startup class;
Write the thread pool configuration class, and don't forget the @Configuration and @Bean annotations;
Write the business that needs to be executed asynchronously and place it in a separate class (you can define it as a service because spring is required to manage it);
Call an asynchronously executed service in a business service. Note that this is the key point. You cannot write asynchronously executed code directly in a business service, otherwise you cannot execute asynchronously (this is why you place asynchronous code separately);
Use steps
Create a configuration for the thread pool and let Spring Boot load to define how to create a ThreadPoolTaskExecutor. Use the @Configuration and @EnableAsync annotations to indicate that this is a configuration class and that it is a configuration class for the thread pool
@Configuration @EnableAsync public class ExecutorConfig { private static final Logger logger = LoggerFactory.getLogger(ExecutorConfig.class); @Value("${async.executor.thread.core_pool_size}") private int corePoolSize; @Value("${async.executor.thread.max_pool_size}") private int maxPoolSize; @Value("${async.executor.thread.queue_capacity}") private int queueCapacity; @Value("${async.executor.thread.name.prefix}") private String namePrefix; @Bean(name = "asyncServiceExecutor") public Executor asyncServiceExecutor() { logger.info("start asyncServiceExecutor"); ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); //Configure Number of Core Threads executor.setCorePoolSize(corePoolSize); //Configure Maximum Threads executor.setMaxPoolSize(maxPoolSize); //Configure Queue Size executor.setQueueCapacity(queueCapacity); //Configure the name prefix of threads in the thread pool executor.setThreadNamePrefix(namePrefix); // rejection-policy: how to handle new tasks when the pool has reached max size // CALLER_RUNS: Do not execute tasks in a new thread, but have the caller's thread execute executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); //Perform Initialization executor.initialize(); return executor; } }
@Value is configurable in application.properties and can be freely defined with reference to configuration
# Asynchronous Thread Configuration # Configure Number of Core Threads async.executor.thread.core_pool_size = 5 # Configure Maximum Threads async.executor.thread.max_pool_size = 5 # Configure Queue Size async.executor.thread.queue_capacity = 99999 # Configure the name prefix of threads in the thread pool async.executor.thread.name.prefix = async-service-
Create a Service interface, which is an interface for asynchronous threads
public interface AsyncService { /** * Execute Asynchronous Tasks * You can add your own parameters if you want, so I'll do a test demonstration here */ void executeAsync(); }
Implementation Class
@Service public class AsyncServiceImpl implements AsyncService { private static final Logger logger = LoggerFactory.getLogger(AsyncServiceImpl.class); @Override @Async("asyncServiceExecutor") public void executeAsync() { logger.info("start executeAsync"); System.out.println("What asynchronous threads do"); System.out.println("You can do time-consuming things like bulk inserts here"); logger.info("end executeAsync"); } }
Asynchronize the service at the Service level by adding the comment @Async("asyncServiceExecutor") to the executeAsync() method, which is the method name from the previous ExecutorConfig.java, indicating that the thread pool into which the executeAsync method entered was created by the asyncServiceExecutor method.
The next step is to inject this Service in Controller or somewhere with the comment @Autowired
@Autowired private AsyncService asyncService; @GetMapping("/async") public void async(){ asyncService.executeAsync(); }
Use postmain or other tools to test requests multiple times
From the above logs, we can see that [async-service-] has multiple threads, which are apparently executed in the thread pool we configured, and that the start and end logs of the controller are printed continuously in each request, indicating that each request responds quickly and time-consuming operations are left to the threads in the thread pool to execute asynchronously.
Although we've used the thread pool, it's not clear how many threads were executing and how many were waiting in the queue at that time. Here I've created a subclass of ThreadPoolTaskExecutor that prints out the current thread pool's health each time a thread is committed
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.util.concurrent.ListenableFuture; import java.util.concurrent.Callable; import java.util.concurrent.Future; import java.util.concurrent.ThreadPoolExecutor; public class VisiableThreadPoolTaskExecutor extends ThreadPoolTaskExecutor { private static final Logger logger = LoggerFactory.getLogger(VisiableThreadPoolTaskExecutor.class); private void showThreadPoolInfo(String prefix) { ThreadPoolExecutor threadPoolExecutor = getThreadPoolExecutor(); if (null == threadPoolExecutor) { return; } logger.info("{}, {},taskCount [{}], completedTaskCount [{}], activeCount [{}], queueSize [{}]", this.getThreadNamePrefix(), prefix, threadPoolExecutor.getTaskCount(), threadPoolExecutor.getCompletedTaskCount(), threadPoolExecutor.getActiveCount(), threadPoolExecutor.getQueue().size()); } @Override public void execute(Runnable task) { showThreadPoolInfo("1. do execute"); super.execute(task); } @Override public void execute(Runnable task, long startTimeout) { showThreadPoolInfo("2. do execute"); super.execute(task, startTimeout); } @Override public Future<?> submit(Runnable task) { showThreadPoolInfo("1. do submit"); return super.submit(task); } @Override public <T> Future<T> submit(Callable<T> task) { showThreadPoolInfo("2. do submit"); return super.submit(task); } @Override public ListenableFuture<?> submitListenable(Runnable task) { showThreadPoolInfo("1. do submitListenable"); return super.submitListenable(task); } @Override public <T> ListenableFuture<T> submitListenable(Callable<T> task) { showThreadPoolInfo("2. do submitListenable"); return super.submitListenable(task); } }
As shown above, the showThreadPoolInfo method prints out the total number of tasks, completed threads, active threads, queue size, and then overrides the execute, submit methods of the parent class, calling the showThreadPoolInfo method inside, so that each time a task is committed to the thread pool, the basic condition of the current thread pool is printed to the log.
Modify the asyncServiceExecutor method of ExecutorConfig.java, changing ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor() to ThreadPoolTaskExecutor executor = new VisiableThreadPoolTaskExecutor()
@Bean(name = "asyncServiceExecutor") public Executor asyncServiceExecutor() { logger.info("start asyncServiceExecutor"); //Modify here ThreadPoolTaskExecutor executor = new VisiableThreadPoolTaskExecutor(); //Configure Number of Core Threads executor.setCorePoolSize(corePoolSize); //Configure Maximum Threads executor.setMaxPoolSize(maxPoolSize); //Configure Queue Size executor.setQueueCapacity(queueCapacity); //Configure the name prefix of threads in the thread pool executor.setThreadNamePrefix(namePrefix); // rejection-policy: how to handle new tasks when the pool has reached max size // CALLER_RUNS: Do not execute tasks in a new thread, but have the caller's thread execute executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); //Perform Initialization executor.initialize(); return executor; }
Start the project test again
When submitting a task to the thread pool, the method called submit(Callable task). Currently, there are 3 tasks submitted and 3 completed. Currently, there are 0 threads working on the task, and 0 tasks remaining in the queue. The basic situation of the thread pool is the same.
@Async Multithreaded Get Return Value
// Asynchronous code execution @Service("asyncExecutorTest") public class AsyncExecutorTest { // Asynchronous execution method, custom thread pool class name in comment @Async("asyncServiceExecutor") public Future<Integer> test1(Integer i) throws InterruptedException { Thread.sleep(100); System.out.println("@Async implement: " + i); return new AsyncResult(i); } // Called here in another way, as described in the following service3 method public Integer test2(Integer i) throws InterruptedException { Thread.sleep(100); System.out.println(" excute.run implement: " + i); return i; } }
// Business service @Service("asyncExcutorService") public class AsyncExcutorService { @Autowired AsyncExecutorTest asyncExecutorTest; @Autowired Executor localBootAsyncExecutor; // Test executes asynchronously with no return value public void service1(){ System.out.println("service1 implement----->"); for (int i = 0; i < 50; i++) { try { asyncExecutorTest.test1(i); } catch (InterruptedException e) { System.out.println("service1 Execution error"); } } System.out.println("service1 End----->"); } // Tests executed asynchronously with return values public void service2(){ long l = System.currentTimeMillis(); System.out.println("service2 implement----->"); List<Future> result = new ArrayList<>(); try { for (int i = 0; i < 300; i++) { Future<Integer> integerFuture = asyncExecutorTest.test1(i); result.add(integerFuture); } for (Future future : result) { System.out.println(future.get()); } } catch (InterruptedException | ExecutionException e) { System.out.println("service2 Execution error"); } System.out.println("service2 End----->" + (System.currentTimeMillis() - l)); } // Tests executed asynchronously with return values public void service3(){ long l = System.currentTimeMillis(); List<Integer> result = new ArrayList<>(); try { System.out.println("service3 implement----->"); int total = 300; CountDownLatch latch = new CountDownLatch(total); for (int i = 0; i < total; i++) { final int y = i; localBootAsyncExecutor.execute(() -> { try { result.add(asyncExecutorTest.test2(y)); } catch (InterruptedException e) { System.out.println("service3 Execution error"); } finally { latch.countDown(); } }); } latch.await(); } catch (InterruptedException e) { System.out.println("service3 Execution error"); } System.out.println("service3 End----->" + (System.currentTimeMillis() - l)); } }
Here's the difference between service1 and service2:
-
Both are executed using a thread pool
-
Service 1 does business without returning data or waiting for the main thread
-
service2 needs to return data, and the main thread needs to wait for the result (note that the return value can only be Future, and then. get() to get it, otherwise it cannot be executed asynchronously)
-
Service 3 can also return data, but it's more cumbersome to write.The return value is directly what you want, unlike service2, which requires data extraction once.