警惕!SpringBoot錯(cuò)誤發(fā)布事件,造成死鎖Deadlock
環(huán)境:SpringBoot3.2.5
1. 死鎖復(fù)現(xiàn)
1.1 自定義事件監(jiān)聽
public class PackApplicationEvent extends ApplicationEvent {
private String message ;
public PackApplicationEvent(String message, Object source) {
super(source) ;
this.message = message ;
}
public String getMessage() {
return message ;
}
}
自定義事件,接收消息及相關(guān)數(shù)據(jù)
1.2 自定義事件監(jiān)聽
@Component
public class PackApplicationListener implements ApplicationListener<PackApplicationEvent> {
@Override
public void onApplicationEvent(PackApplicationEvent event) {
System.out.printf("接收到事件消息: %s, 數(shù)據(jù): %s%n", event.getMessage(), event.getSource().toString()) ;
// TODO
}
}
該事件監(jiān)聽器只打印了信息。
1.3 發(fā)布事件
@Component
public class EventProcessor {
public EventProcessor(ApplicationEventPublisher eventPublisher) {
Thread t = new Thread(() -> {
eventPublisher.publishEvent(new PackApplicationEvent("自定義事件", EventProcessor.this));
});
t.start() ;
try {
System.out.println("線程啟動(dòng),等待執(zhí)行完成...") ;
t.join() ;
} catch (InterruptedException e) {
System.err.printf("線程中斷: %s, 錯(cuò)誤: %s%n", Thread.currentThread().getName(), e.getMessage()) ;
}
}
}
該Bean在構(gòu)造函數(shù)中新啟一個(gè)線程發(fā)布事件,同時(shí)通過join方法等待線程執(zhí)行完成。
上面的程序運(yùn)行后,發(fā)現(xiàn)輸出了上面的打印內(nèi)容后應(yīng)用沒有繼續(xù)運(yùn)行。打印整個(gè)線程棧(通過jstack命令查看),如下:
圖片
根據(jù)線程信息,main線程在創(chuàng)建EventProcessor對象時(shí),會(huì)先持有DefaultSingletonBeanRegistry.singletonObjects這個(gè)ConcurrentHashMap對象鎖接著創(chuàng)建EventProcessor對象實(shí)例,在調(diào)用該對象的構(gòu)造函數(shù)時(shí),啟動(dòng)新的線程Thread-1,該線程發(fā)布事件同時(shí)通過join方法等待T1這個(gè)線程完成,在發(fā)布事件時(shí)Spring容器會(huì)獲取所有的ApplicationListener,此時(shí)就會(huì)又創(chuàng)建PackApplicationListener對象,創(chuàng)建該對象同樣要獲取singletonObjects鎖對象,這樣就造成了死鎖。
主線程
圖片
主線程創(chuàng)建EventProcessor對象。
Thread-1線程
圖片
Thread-1線程獲取容器中的ApplicationListener類型的bean,該過程將執(zhí)行到如下步驟:
圖片
main線程持有singletonObjects鎖,Thread-1線程又期望獲取到該鎖,但是main線程還要等待Thread-1線程執(zhí)行完成。這死鎖了。
以上是對死鎖的復(fù)現(xiàn)及原因進(jìn)行了分析,接下來進(jìn)行問題的解決。
2. 解決問題
2.1 解決方式1
不要在構(gòu)造函數(shù)中發(fā)布事件,而是應(yīng)該在所有的單例對象都創(chuàng)建完后再執(zhí)行,也就是實(shí)現(xiàn)SmartInitializingSingleton接口,該接口對應(yīng)的回調(diào)方法會(huì)在所有的單例bean都創(chuàng)建完以后執(zhí)行,這樣就不會(huì)再出現(xiàn)deadlock問題。
@Component
public class EventProcessor implements SmartInitializingSingleton {
private final ApplicationEventPublisher eventPublisher ;
public EventProcessor(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher ;
}
@Override
public void afterSingletonsInstantiated() {
Thread t = new Thread(() -> {
eventPublisher.publishEvent(new PackApplicationEvent("自定義事件", EventProcessor.this));
});
t.start() ;
try {
t.join() ;
} catch (InterruptedException e) {
System.err.printf("線程中斷: %s, 錯(cuò)誤: %s%n", Thread.currentThread().getName(), e.getMessage()) ;
}
}
}
這樣改造后容器能正常的啟動(dòng),同時(shí)事件也正常的發(fā)布&監(jiān)聽。
afterSingletonsInstantiated方法的調(diào)用在如下:
public class DefaultListableBeanFactory {
public void preInstantiateSingletons() {
for (String beanName : beanNames) {
// 創(chuàng)建單例bean
getBean(beanName);
}
// 單例bean創(chuàng)建完成以后,執(zhí)行afterSingletonsInstantiated回調(diào)方法
for (String beanName : beanNames) {
Object singletonInstance = getSingleton(beanName);
if (singletonInstance instanceof SmartInitializingSingleton smartSingleton) {
smartSingleton.afterSingletonsInstantiated();
}
}
}
}
以上就不會(huì)在出現(xiàn)鎖問題。
2.2 解決方式2
升級Spring版本到Spring6.2(目前并沒有正式發(fā)布),你仍然可以使用6.2.0-SNAPSHOT版本,該版本通過多線程方式初始化Bean對象,這樣就不會(huì)出現(xiàn)deadlock問題。