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() 根本不会调用 运行 方法...
编辑:
所以我进一步研究了这个问题,这就是我发现它停止工作的地方:
- 我用了scheduledFuture.get()等待任务完成
- AnalysisCleaningThread 的 运行() 方法中的第一行代码被调用,它应该调用服务来检索分析列表
- 调用CglibAopProxy拦截方法
- ReflectiveMethodInvocation -> TransactionInterceptor -> TransactionAspectSupport -> DefaultListableBeanFactory -> AbstractBeanFactory 被调用以按类型搜索和匹配 PlatformTransactionManager bean
- 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
,那么它就不会发生。
我正在使用 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() 根本不会调用 运行 方法...
编辑:
所以我进一步研究了这个问题,这就是我发现它停止工作的地方:
- 我用了scheduledFuture.get()等待任务完成
- AnalysisCleaningThread 的 运行() 方法中的第一行代码被调用,它应该调用服务来检索分析列表
- 调用CglibAopProxy拦截方法
- ReflectiveMethodInvocation -> TransactionInterceptor -> TransactionAspectSupport -> DefaultListableBeanFactory -> AbstractBeanFactory 被调用以按类型搜索和匹配 PlatformTransactionManager bean
- 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
,那么它就不会发生。