spring boot,

spring自带线程池使用不当导致的死锁问题

qihaiyan qihaiyan Follow Jan 10, 2021 · 2 mins read

Spring自带线程池使用很方便,不过在相对复杂的并发编程场景中,使用时还是需要根据使用场景仔细考虑配置,否则可能会遇到本文中提及的坑。 具体的代码参照 示例项目 https://github.com/qihaiyan/springcamp/tree/master/spring-taskexecutor-block

一、概述

spring自带线程池有2个核心配置,一个是线程池的大小,一个是队列的大小。 ThredPoolTaskExcutor的处理流程: 新建线程并处理请求,直到线程数大小等于corePoolSize; 将请求放入workQueue中,线程池中的空闲线程去workQueue中取任务并处理; 当workQueue满时,就新建线程并处理请求,当线程池子大小大小等于maximumPoolSize时,会用RejectedExecutionHandler来做拒绝处理。

Reject策略有四种:

(1)AbortPolicy策略,是默认的策略,拒绝请求并抛出异常RejectedExecutionException。

(2)CallerRunsPolicy策略 ,由调用线程执行任务.

(3)DiscardPolicy策略,拒绝请求但不抛出异常.

(4)DiscardOldestPolicy策略,丢弃最早进入队列的任务.

二、多个异步处理共用同一个线程池的异常情况

模拟一个耗时的操作,该操作通过Async注解设置为异步执行。Async会默认使用名为taskExecutor的线程池。该操作返回一个CompletableFuture,后续的处理中会等待该异步操作执行完成。

@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);
    }
}

设置线程池,将线程池大小设置为2,队列设置为一个比线程池大的值,此处为10。当队列大小大于等于线程池大小时,就会出现本文遇到的程序阻塞的问题。

    @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;
    }
}

并发处理:

    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();
            }
        }

三、问题分析

程序启动后,很快就会阻塞,通过jstack查看线程状态,发现taskExecutor-1、taskExecutor-2、main三个线程都处在WAITING状态,等待CompletableFuture.join方法执行完成。

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)

通过分析程序的执行过程,不难发现阻塞的原因。 由于线程池设置的Queue的大小大于线程池的大小,当线程池满时,delayFoo方法会处在队列中,随着程序的执行,总会出现线程池中都是CompletableFuture.join方法,队列中都是delayFoo方法的情况。

这时候线程中的join方法在等待队列中的delayFoo方法执行完成,而队列中的delayFoo方法由于等不到可用线程而无法执行,整个程序就陷入了死锁状态。

解决的方法也很简单,就是将队列的大小设置为小于线程数的大小,这样队列中的方法就有机会拿到线程,从而不会因为线程占满而进入死锁状态。

qihaiyan
Written by qihaiyan
业精于勤而荒于嬉,行成于思而毁于随