java – Spring Application的@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.");
}
}
}
我在第一行run方法和第二行上设置了一个突破点.如果我使用ScheduledFuture.get(),则调用第一行,然后调用JPA存储库方法,但它永远不会返回…它不会在控制台中生成查询…
如果我使用ScheduledFuture.isDone(),则根本不会调用run方法…
编辑:
所以我进一步挖掘了这个问题,这就是我发现它停止工作的地方:
>我使用了scheduledFuture.get()来等待任务完成
>调用AnalysisCleaningThread的run()方法中的第一行代码,该代码应调用服务以检索分析列表
>调用CglibAopProxy来拦截该方法
> ReflectiveMethodInvocation – > TransactionInterceptor – > TransactionAspectSupport – > DefaultListableBeanFactory – >调用AbstractBeanFactory以按类型搜索和匹配PlatformTransactionManager bean
>使用beanName“main”调用DefaultSingletonBeanRegistry.getSingleton,并在第187行同步(this.singletonObjects)应用程序暂停,并且永远不会继续
从我的观点来看,似乎this.singletonObjects目前正在使用,因此线程不能以某种方式继续…
解决方法:
所以我已经做了很多研究,因为发生了这个问题,最后找到了解决我的罕见情况的方法.
我首先注意到的是,如果没有future.get(),AnalysisCleaningThread确实运行没有任何问题,但是run方法花费了2秒钟来执行第一行,所以我认为必须在后台进行一些事情.最终可以进行数据库调用.
我在原始问题编辑中通过调试发现的是,应用程序在第93行的DefaultSingletonBeanRegistry.getSingleton方法中的同步块同步(this.singletonObjects)处停止,因此必须持有该锁定对象.一旦调用DefaultSingletonBeanRegistry.getSingleton的迭代方法将“main”作为参数“beanName”传递给getSingleton,它就会在该行停止.
BTW该方法(或更好的方法链)被调用以获得PlatformTransactionManager bean的实例以进行该服务(数据库)调用.
我的第一个想法是,它必定是一个僵局.
最后的想法
根据我的理解,bean在其生命周期内仍然没有最终准备好(仍然在其@PostConstruct init()方法中).当spring尝试获取平台事务管理器的实例以便进行数据库查询时,应用程序会死锁.它实际上是死锁,因为在迭代所有bean名称以找到PlatformTansactionManager时,它还会尝试解析当前正在等待的“main”bean,因为@PostConstruct方法中的future.get().因此它无法获取实例并且永远等待释放锁.
解
因为我不想将该代码放在另一个类中,因为Main.class是我的入口点,所以我开始寻找一个钩子,它在应用程序完全启动后启动任务.
我弄乱了@EventListener,在我的情况下,它监听ApplicationReadyEvent.class和voilà,它有效.这是我的代码解决方案.
@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方法执行spring数据存储库调用 – 在极少数情况下 – 如果使用@PostConstruct注释的方法没有结束则会死锁
在spring之前可以获取PlatformTransactionManager bean来执行spring数据存储库查询.无论是无限循环还是future.get()方法都无关紧要….只有当迭代所有已注册的beanNames并最终调用DefaultSingletonBeanRegistry.getSingleton以找到PlatformTransactionManager bean的方法调用带有当前位于@PostConstruct方法中的bean名称的getSingleton时,才会发生这种情况.如果在此之前找到PlatformTransactionManager,则不会发生.
我希望它可以理解,而且我的英语很好,可以解释它.
标签:java,spring,future,spring-data-jpa,taskscheduler 来源: https://codeday.me/bug/20190701/1347855.html