环境:SpringBoot3.2.5
1. 死锁复现
1.1 自定义事件监听
public class PackApplicationEvent extends ApplicationEvent {
private String message ;
public PackApplicationEvent(String message, Object source) {
super(source) ;
this.message = message ;
}
public String getMessage() {
return message ;
}
}
自定义事件,接收消息及相关数据
1.2 自定义事件监听
@Component
public class PackApplicationListener implements ApplicationListener<PackApplicationEvent> {
@Override
public void onApplicationEvent(PackApplicationEvent event) {
System.out.printf("接收到事件消息: %s, 数据: %s%n", event.getMessage(), event.getSource().toString()) ;
// TODO
}
}
该事件监听器只打印了信息。
1.3 发布事件
@Component
public class EventProcessor {
public EventProcessor(ApplicationEventPublisher eventPublisher) {
Thread t = new Thread(() -> {
eventPublisher.publishEvent(new PackApplicationEvent("自定义事件", EventProcessor.this));
});
t.start() ;
try {
System.out.println("线程启动,等待执行完成...") ;
t.join() ;
} catch (InterruptedException e) {
System.err.printf("线程中断: %s, 错误: %s%n", Thread.currentThread().getName(), e.getMessage()) ;
}
}
}
该Bean在构造函数中新启一个线程发布事件,同时通过join方法等待线程执行完成。
上面的程序运行后,发现输出了上面的打印内容后应用没有继续运行。打印整个线程栈(通过jstack命令查看),如下:
图片
根据线程信息,main线程在创建EventProcessor对象时,会先持有DefaultSingletonBeanRegistry.singletonObjects这个ConcurrentHashMap对象锁接着创建EventProcessor对象实例,在调用该对象的构造函数时,启动新的线程Thread-1,该线程发布事件同时通过join方法等待T1这个线程完成,在发布事件时Spring容器会获取所有的ApplicationListener,此时就会又创建PackApplicationListener对象,创建该对象同样要获取singletonObjects锁对象,这样就造成了死锁。
主线程
图片
主线程创建EventProcessor对象。
Thread-1线程
图片
Thread-1线程获取容器中的ApplicationListener类型的bean,该过程将执行到如下步骤:
图片
main线程持有singletonObjects锁,Thread-1线程又期望获取到该锁,但是main线程还要等待Thread-1线程执行完成。这死锁了。
以上是对死锁的复现及原因进行了分析,接下来进行问题的解决。
2. 解决问题
2.1 解决方式1
不要在构造函数中发布事件,而是应该在所有的单例对象都创建完后再执行,也就是实现SmartInitializingSingleton接口,该接口对应的回调方法会在所有的单例bean都创建完以后执行,这样就不会再出现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, 错误: %s%n", Thread.currentThread().getName(), e.getMessage()) ;
}
}
}
这样改造后容器能正常的启动,同时事件也正常的发布&监听。
afterSingletonsInstantiated方法的调用在如下:
public class DefaultListableBeanFactory {
public void preInstantiateSingletons() {
for (String beanName : beanNames) {
// 创建单例bean
getBean(beanName);
}
// 单例bean创建完成以后,执行afterSingletonsInstantiated回调方法
for (String beanName : beanNames) {
Object singletonInstance = getSingleton(beanName);
if (singletonInstance instanceof SmartInitializingSingleton smartSingleton) {
smartSingleton.afterSingletonsInstantiated();
}
}
}
}
以上就不会在出现锁问题。
2.2 解决方式2
升级Spring版本到Spring6.2(目前并没有正式发布),你仍然可以使用6.2.0-SNAPSHOT版本,该版本通过多线程方式初始化Bean对象,这样就不会出现deadlock问题。