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

开发 前端
升级Spring版本到Spring6.2(目前并没有正式发布),你仍然可以使用6.2.0-SNAPSHOT版本,该版本通过多线程方式初始化Bean对象,这样就不会出现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问题。


责任编辑:武晓燕 来源: Spring全家桶实战案例源码
相关推荐

2009-03-13 10:12:18

变量共享匿名方法.NET

2023-02-08 07:04:20

死锁面试官单元

2024-08-08 09:05:54

2023-04-03 11:23:00

IT 主管首席信息官

2023-11-02 08:10:13

框架Spring程序事件

2024-08-01 09:57:17

DELETE死锁工具

2024-07-04 00:30:17

2011-07-27 10:37:28

IT安全安全错误

2013-01-21 09:41:00

路由器设备故障设置参数

2011-05-31 15:19:17

2021-08-12 11:37:23

数据分析错误

2010-06-07 21:26:22

赛门铁克诺顿网络安全

2012-06-06 15:38:44

2018-10-16 16:00:39

数据库锁舞MySQL

2024-06-03 08:40:31

2021-08-24 08:01:15

死锁工具多线编程

2021-08-28 09:04:54

死锁顺序锁轮询锁

2024-03-27 08:18:02

Spring映射HTML

2009-06-26 10:43:50

互联网故障路由器

2016-05-11 10:56:52

数据崇拜
点赞
收藏

51CTO技术栈公众号