Spring 引导应用程序的 @PostConstruct 方法中的死锁

Deadlock in Spring Boot Application's @PostConstruct Method

我正在使用 Spring TaskScheduler 在应用程序启动时安排任务(显然...)。

TaskScheduler 在我的 SpringConfig:

中创建
@Configuration
@EnableTransactionManagement
public class SpringConfig {

    @Bean
    public TaskScheduler taskScheduler() {
        return new ThreadPoolTaskScheduler();
    }

}

spring 引导应用程序在我的 Main.class 中启动并安排任务 @PostConstruct

@SpringBootApplication
@ComponentScan("...")
@EntityScan("...")
@EnableJpaRepositories("... .repositories")
@EnableAutoConfiguration
@PropertySources(value = {@PropertySource("classpath:application.properties")})
public class Main {

    private final static Logger LOGGER = LoggerFactory.getLogger(Main.class);

    private static SpringApplication application = new SpringApplication(Main.class);

    private TaskScheduler taskScheduler;

    private AnalysisCleaningThread cleaningThread;

    @Inject
    public void setCleaningThread(AnalysisCleaningThread cleaningThread) {
        this.cleaningThread = cleaningThread;
    }

    @Inject
    public void setTaskScheduler(TaskScheduler taskScheduler) {
        this.taskScheduler = taskScheduler;
    }

    public static void main(String[] args)
            throws Exception {

        try {

            //Do some setup

            application.run(args);

        } catch (Exception e) {

            LOGGER.error(e.getMessage(), e);
        }
    }


    @PostConstruct
    public void init()
            throws Exception {

        //Do some setup as well

        ScheduledFuture scheduledFuture = null;
        LOGGER.info("********** Scheduling one time Cleaning Thread. Starting in 5 seconds **********");
        Date nowPlus5Seconds = Date.from(LocalTime.now().plus(5, ChronoUnit.SECONDS).atDate(LocalDate.now()).atZone(ZoneId.systemDefault()).toInstant());
        scheduledFuture = this.taskScheduler.schedule(this.cleaningThread, nowPlus5Seconds);




        while (true) {
           //Somehow blocks thread from running
           if (scheduledFuture.isDone()) {
               break;
           }
           Thread.sleep(2000);
        }



        //schedule next periodic thread

}

应用程序必须等待线程完成,因为它的任务是在应用程序意外关闭后清理脏数据库条目。下一个任务是拾取清理过的条目并再次处理它们。 清洗线程实现如下:

@Named
@Singleton
public class AnalysisCleaningThread implements Runnable {

    private static Logger LOGGER = LoggerFactory.getLogger(AnalysisCleaningThread.class);

    private AnalysisService analysisService;

    @Inject
    public void setAnalysisService(AnalysisService analysisService) {
        this.analysisService = analysisService;
    }

    @Override
    public void run() {
        List<Analysis> dirtyAnalyses = analysisService.findAllDirtyAnalyses();
        if(dirtyAnalyses != null && dirtyAnalyses.size() > 0) {
            LOGGER.info("Found " + dirtyAnalyses.size() + " dirty analyses. Cleaning... ");
            for (Analysis currentAnalysis : dirtyAnalyses) {
                //Reset AnalysisState so it is picked up by ProcessingThread on next run
                currentAnalysis.setAnalysisState(AnalysisState.CREATED);
            }
            analysisService.saveAll(dirtyAnalyses);
        } else {
            LOGGER.info("No dirty analyses found.");
        }
    }

}

我在 运行 方法的第一行和第二行放置了一个断点。如果我使用 ScheduledFuture.get(),第一行被调用,然后调用 JPA 存储库方法,但它永远不会 returns...它不会在控制台中生成查询...

如果我使用 ScheduledFuture.isDone() 根本不会调用 运行 方法...

编辑:

所以我进一步研究了这个问题,这就是我发现它停止工作的地方:

  1. 我用了scheduledFuture.get()等待任务完成
  2. AnalysisCleaningThread 的 运行() 方法中的第一行代码被调用,它应该调用服务来检索分析列表
  3. 调用CglibAopProxy拦截方法
  4. ReflectiveMethodInvocation -> TransactionInterceptor -> TransactionAspectSupport -> DefaultListableBeanFactory -> AbstractBeanFactory 被调用以按类型搜索和匹配 PlatformTransactionManager bean
  5. DefaultSingletonBeanRegistry.getSingleton 使用 beanName "main" 调用,在 line 187 synchronized(this.singletonObjects) 应用程序暂停,永不继续

从我的角度来看,似乎 this.singletonObjects 当前正在使用中,因此线程无法以某种方式继续...

自从那个问题发生后,我做了很多研究,终于找到了解决我这个罕见案例的方法。

我首先注意到的是,在没有 future.get() 的情况下,AnalysisCleaningThread 执行 运行 没有任何问题,但是 运行 方法花费了大约 2 秒的时间第一行要执行,所以我认为在最终进行数据库调用之前一定有一些事情在后台发生。

我在最初的问题编辑中通过调试发现,应用程序在第 93 行 DefaultSingletonBeanRegistry.getSingleton 方法中的同步块 synchronized(this.singletonObjects) 处停止[=51] =] 所以一定有什么东西持有那个锁对象。一旦调用 DefaultSingletonBeanRegistry.getSingleton 的迭代方法将 "main" 作为参数 "beanName" 传递给 getSingleton.

,它实际上就停在了那一行

顺便说一句,调用该方法(或更好的方法链)是为了获取 PlatformTransactionManager bean 的实例以进行该服务(数据库)调用。

当时我的第一个想法是,这一定是一个僵局。

最后的想法

根据我的理解,bean 在其生命周期内仍未最终准备好(仍在其 @PostConstruct init() 方法中)。当 spring 试图获取平台事务管理器的一个实例以便进行数据库查询时,应用程序出现死锁。它实际上是死锁,因为在遍历所有 bean 名称以查找 PlatformTansactionManager 时,它还尝试解析由于 @PostConstruct 方法中的 future.get() 而当前正在等待的 "main" bean。因此它无法获得一个实例并且永远等待锁被释放。

解决方案

因为我不想将该代码放在另一个 class 中,因为 Main.class 是我的入口点,所以我开始寻找一个挂钩,它在应用程序完全完成后启动任务启动。

我无意中发现了 @EventListener,在我的例子中它监听了 ApplicationReadyEvent.class,瞧,它起作用了。这是我的代码解决方案。

@SpringBootApplication
@ComponentScan("de. ... .analysis")
@EntityScan("de. ... .persistence")
@EnableJpaRepositories("de. ... .persistence.repositories")
@EnableAutoConfiguration
@PropertySources(value = {@PropertySource("classpath:application.properties")})
public class Main {

    private final static Logger LOGGER = LoggerFactory.getLogger(Main.class);

    private static SpringApplication application = new SpringApplication(Main.class);

    private TaskScheduler taskScheduler;

    private AnalysisProcessingThread processingThread;

    private AnalysisCleaningThread cleaningThread;


    @Inject
    public void setProcessingThread(AnalysisProcessingThread processingThread) {
        this.processingThread = processingThread;
    }

    @Inject
    public void setCleaningThread(AnalysisCleaningThread cleaningThread) {
        this.cleaningThread = cleaningThread;
    }

    @Inject
    public void setTaskScheduler(TaskScheduler taskScheduler) {
        this.taskScheduler = taskScheduler;
    }

    public static void main(String[] args)
            throws Exception {

        try {

            //Do some setup

            application.run(args);

        } catch (Exception e) {

            LOGGER.error(e.getMessage(), e);
        }
    }

    @PostConstruct
    public void init() throws Exception {

        //Do some other setup

    }

    @EventListener(ApplicationReadyEvent.class)
    public void startAndScheduleTasks() {
        ScheduledFuture scheduledFuture = null;
        LOGGER.info("********** Scheduling one time Cleaning Thread. Starting in 5 seconds **********");
        Date nowPlus5Seconds = Date.from(LocalTime.now().plus(5, ChronoUnit.SECONDS).atDate(LocalDate.now()).atZone(ZoneId.systemDefault()).toInstant());
        scheduledFuture = this.taskScheduler.schedule(this.cleaningThread, nowPlus5Seconds);
        try {
            scheduledFuture.get();
        } catch (InterruptedException | ExecutionException e) {
            LOGGER.error("********** Cleaning Thread did not finish as expected! Stopping thread. Dirty analyses may still remain in database **********", e);
            scheduledFuture.cancel(true);
        }
  }
}

总结

如果用 @PostConstruct 注释的方法没有结束,在极少数情况下,从 @PostConstruct 方法执行 spring 数据存储库调用可能会死锁 在 spring 之前可以获取 PlatformTransactionManager bean 来执行 spring 数据存储库查询。它是无限循环还是 future.get() 方法都没有关系...。它也仅在迭代所有已注册的 beanName 并最终调用 DefaultSingletonBeanRegistry.getSingleton 以查找 PlatformTransactionManager bean 的方法使用当前在 @PostConstruct 方法中的 bean 名称调用 getSingleton 时发生。如果它找到了之前的 PlatformTransactionManager,那么它就不会发生。