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



我正在使用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 存储库方法,但它永远不会返回...它不会在控制台中生成查询...

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

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

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

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

因此,自从这个问题发生以来,我做了很多研究,最终找到了解决我罕见情况的方法。

我首先注意到的是,如果没有 future.get(),AnalysisCleaningThread确实运行没有任何问题,但运行方法需要大约 2 秒才能执行第一行,所以我认为在数据库调用之前,后台一定发生了什么事情最终可以进行。

我在最初的问题编辑中通过调试发现,应用程序在第93 行DefaultSingletonBeanRegistry.getSingleton方法中的同步块synchronized(this.singletonObjects)处停止,因此必须有东西持有该锁定对象。一旦调用DefaultSingletonBeanRegistry.getSingleton迭代方法将"main"作为参数"beanName"传递到getSingleton,它实际上就停在那行。

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

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

结语

据我了解,Bean 在其生命周期内仍未最终准备就绪(仍处于其@PostConstructinit()方法中)。当 spring 尝试获取平台事务管理器的实例以进行数据库查询时,应用程序将死锁。它实际上死锁了,因为在遍历所有 bean 名称以查找 PlatformTansactionManager 时,它还尝试解析当前正在等待的"主"bean,因为其 @PostConstruct 方法中的 future.get()。因此,它无法获取实例,并且永远等待释放锁。

溶液

由于我不想将该代码放在另一个类中,因为 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 可以获取PlatformTransactionManagerBean 之前执行 Spring 数据存储库查询。无论是无限循环还是 future.get() 方法都没关系......仅当该方法遍历所有注册的 beanNames 并最终调用DefaultSingletonBeanRegistry.getSingleton以查找 PlatformTransactionManager bean 时,使用当前在@PostConstruct方法中的 bean 名称调用 getSingleton 时,也会发生这种情况。如果它在那之前找到PlatformTransactionManager,那么它就不会发生。

最新更新