警惕!SpringBoot错误发布事件,造成死锁Deadlock

环境: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问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值