Deadlock problem caused by improper use of spring ThreadPoolTaskExecutor

Spring's built-in thread pool is very convenient to use, but in relatively complex concurrent programming scenarios, you still need to carefully consider the configuration according to the usage scenario, otherwise you may encounter the pitfalls mentioned in this article.
For specific code, refer to the sample project https://github.com/qihaiyan/springcamp/tree/master/spring-taskexecutor-block

I. Overview

Spring's own thread pool has two core configurations, one is the size of the thread pool and the other is the size of the queue.
The processing flow of ThredPoolTaskExcutor:
Create a new thread and process the request until the number of threads is equal to the corePoolSize;
put the request into the workQueue, and the idle thread in the thread pool goes to the workQueue to pick up the task and process it;
when the workQueue is full, create a new thread and process the request , when the thread pool size is equal to maximumPoolSize, RejectedExecutionHandler will be used for rejection processing.

There are four types of Reject strategies:

(1) The AbortPolicy strategy is the default strategy, which rejects the request and throws an exception RejectedExecutionException.

(2) CallerRunsPolicy strategy, the task is executed by the calling thread.

(3) DiscardPolicy strategy, which rejects the request but does not throw an exception.

(4) DiscardOldestPolicy policy, which discards the earliest tasks entering the queue.

2. Abnormal situations where multiple asynchronous processes share the same thread pool

Simulate a time-consuming operation, which is set to be executed asynchronously through the Async annotation. Async will use a thread pool named taskExecutor by default. This operation returns a CompletableFuture, and subsequent processing will wait for the completion of the asynchronous operation.

@Service
public class DelayService {
    
    
    @Async
    public CompletableFuture<String> delayFoo(String v) {
    
    
        try {
    
    
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println(v + " runs in thread: " + Thread.currentThread().getName());
        return CompletableFuture.completedFuture(v);
    }
}

Set the thread pool, set the thread pool size to 2, and set the queue to a value larger than the thread pool, here it is 10. When the queue size is greater than or equal to the thread pool size, the program blocking problem encountered in this article will appear.

    @Bean
    public Executor taskExecutor() {
    
    
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(2);
        executor.setQueueCapacity(10);
        executor.setThreadNamePrefix("taskExecutor-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.initialize();
        return executor;
    }
}

Concurrent processing:

    while (true) {
    
    
        try {
    
    
            CompletableFuture.runAsync(
                () -> CompletableFuture.allOf(Stream.of("1", "2", "3")
                .map(v -> delayService.delayFoo(v))
                .toArray(CompletableFuture[]::new)) // 将数组中的任务提交到线程池中
                .join(), taskExecutor); // 通过join方法等待任务完成
            } catch (Exception e) {
    
    
                e.printStackTrace();
            }
        }

3. Problem analysis

After the program starts, it will be blocked soon. Check the thread status through jstack, and find that the three threads taskExecutor-1, taskExecutor-2, and main are all in the WAITING state, waiting for the CompletableFuture.join method to complete.

priority:5 - threadId:0x00007f7f8eb36800 - nativeId:0x3e03 - nativeId (decimal):15875 - state:WAITING
stackTrace:
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000007961fe548> (a java.util.concurrent.CompletableFuture$Signaller)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.CompletableFuture$Signaller.block(CompletableFuture.java:1693)
at java.util.concurrent.ForkJoinPool.managedBlock(ForkJoinPool.java:3323)
at java.util.concurrent.CompletableFuture.waitingGet(CompletableFuture.java:1729)
at java.util.concurrent.CompletableFuture.join(CompletableFuture.java:1934)

By analyzing the execution process of the program, it is not difficult to find the cause of the blockage.
Since the size of the Queue set by the thread pool is larger than the size of the thread pool, when the thread pool is full, the delayFoo method will be in the queue. With the execution of the program, there will always be CompletableFuture.join methods in the thread pool, and the queue will be is the case for the delayFoo method.

At this time, the join method in the thread is waiting for the execution of the delayFoo method in the queue to complete, but the delayFoo method in the queue cannot be executed because it cannot wait for an available thread, and the whole program falls into a deadlock state.

The solution is also very simple, which is to set the size of the queue to be smaller than the number of threads, so that the methods in the queue have a chance to get the threads, so that they will not enter a deadlock state because the threads are full.

Guess you like

Origin blog.csdn.net/haiyan_qi/article/details/112446160