Start SpringCloud Alibaba from scratch (84) - Thread pool in SpringBoot

Keywords: Java Spring Spring Boot

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:

  1. Both are executed using a thread pool

  2. Service 1 does business without returning data or waiting for the main thread

  3. 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)

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

Posted by kentlim04 on Mon, 20 Sep 2021 09:15:43 -0700