java - Spring Boot 应用程序的@PostConstruct 方法中的死锁

标签 java spring spring-data-jpa future taskscheduler

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

TaskScheduler 在我的 SpringConfig 中创建:

@Configuration
@EnableTransactionManagement
public class SpringConfig {

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

}

spring boot 应用程序在我的 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.");
        }
    }

}

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

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

编辑:

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

  1. 我使用 scheduledFuture.get() 等待任务完成
  2. AnalysisCleaningThread 的 run() 方法中的第一行代码被调用,它应该调用服务来检索分析列表
  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 秒的时间来执行第一行,所以我认为在最终进行数据库调用之前,必须在后台进行某些操作。

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

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

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

最后的想法

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

解决方案

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

我偶然发现了 @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 的方法时发生,并使用当前位于 @ 中的 bean 名称调用 getSingleton PostConstruct 方法。如果它在此之前找到了 PlatformTransactionManager,那么它就不会发生。

关于java - Spring Boot 应用程序的@PostConstruct 方法中的死锁,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48442286/

相关文章:

java - 从 Android 设备调用 .Net 网络服务

java - 使用多维数组进行计数

spring - Azure 服务总线 - Spring Boot 禁用自动启动 (com.microsoft.azure : azure-servicebus-spring-boot-starter)

java - Spring Data JPA (Hibernate) 与动态条件的一对多关系

java - 在 Spring Data JPA 存储库中使用 EntityGraph 进行过滤

java - Perlin 噪声不生成 1 到 -1 之间的数字

java - 获取此异常 : "android.database.sqlite.SQLiteException: no such column: _id: , while compiling" when I have an _id column

java - 使用 Spring Data 和 Hibernate 时如何正确执行后台线程?

mysql - Hibernate不创建连接表

java - 尝试从数据库白色 EhCache 引导过程加载数据时出现异常