簡體   English   中英

Spring Boot Application的@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.");
        }
    }

}

我在第一行run方法和第二行上設置了一個突破點。 如果我使用ScheduledFuture.get(),則調用第一行,然后調用JPA存儲庫方法,但它永遠不會返回...它不會在控制台中生成查詢...

如果我使用ScheduledFuture.isDone(),則根本不會調用run方法...

編輯:

所以我進一步挖掘了這個問題,這就是我發現它停止工作的地方:

  1. 我使用了scheduledFuture.get()來等待任務完成
  2. 調用AnalysisCleaningThread的run()方法中的第一行代碼,該代碼應調用服務以檢索分析列表
  3. 調用CglibAopProxy來攔截該方法
  4. ReflectiveMethodInvocation - > TransactionInterceptor - > TransactionAspectSupport - > DefaultListableBeanFactory - > AbstractBeanFactory被調用以按類型搜索和匹配PlatformTransactionManager bean
  5. 使用beanName “main”調用DefaultSingletonBeanRegistry.getSingleton並在第187行 synchronized(this.singletonObjects)應用程序暫停並且永遠不會繼續

從我的觀點來看,似乎this.singletonObjects目前正在使用,因此線程不能以某種方式繼續...

所以我已經做了很多研究,因為發生了這個問題,最后找到了解決我的罕見情況的方法。

我首先注意到的是,如果沒有future.get(), AnalysisCleaningThread確實運行沒有任何問題,但是run方法花費了2秒鍾來執行第一行,所以我認為必須在后台進行一些事情。最終可以進行數據庫調用。

我在原始問題編輯中通過調試發現的是,應用程序在第93行DefaultSingletonBeanRegistry.getSingleton方法中的同步塊synchronized(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 ,則不會發生。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM