如何在批量创建 `DefaultMessageListenerContainer` 时避免阻塞问题

大家好,我是G探险者!

今天记录一个for循环被阻塞导致后面循环无法执行的问题。

1. 背景问题

在一个分布式消息系统中,我们通常会使用 Spring JMS 提供的 DefaultMessageListenerContainer(简称 DMLC)来监听消息队列。

一个常见的场景是:

  • 系统中存在 多个 MQ 连接工厂(比如不同租户、不同集群、不同数据源的 MQ)。
  • 需要在程序启动时,对这些连接工厂循环创建独立的 DMLC,分别监听对应的队列。

伪代码如下:

java 复制代码
for (ConnectionFactory factory : connectionFactoryList) {
    DefaultMessageListenerContainer container = new DefaultMessageListenerContainer();
    container.setConnectionFactory(factory);
    container.setDestination(...);
    container.setMessageListener(...);
    container.initialize();
    container.start(); // 启动监听容器
}

在正常情况下,这段代码会顺利执行,所有容器都能依次启动。


2. 问题产生

然而在实际运行过程中,有一个棘手的问题:

  • 如果某个 MQ 工厂对应的 Broker 不可用 ,比如因为网络异常、Broker 宕机,或者是常见的 2009 连接异常 (IBM MQ 场景下),那么 DMLC 在 start() 阶段就会阻塞,长时间无法完成启动。
  • 这会导致 for 循环卡死在这一轮,后续的容器创建都无法继续进行。

👉 换句话说,一个坏的 MQ 连接会拖死整个初始化流程


3. 解决思路

为了解决这个问题,我们的目标是:

  • 即使某个 DMLC 启动失败,也不能阻塞整个循环;
  • 最好是让它 异步尝试启动,失败了可以靠自身的自动重连机制继续恢复。

于是就有了一个思路: 把每个容器的启动逻辑放到单独的线程中执行 ,而不是在 for 循环中同步执行。


4. 异步启动容器的实现

我们可以使用 ExecutorService 来包装启动逻辑:

java 复制代码
ExecutorService executor = Executors.newCachedThreadPool();

for (ConnectionFactory factory : connectionFactoryList) {
    executor.submit(() -> {
        try {
            DefaultMessageListenerContainer container = new DefaultMessageListenerContainer();
            container.setConnectionFactory(factory);
            container.setDestination(...);
            container.setMessageListener(...);
            container.afterPropertiesSet(); 
            container.start(); // 独立线程中启动
            // 这里可以保存容器引用,用于后续stop
            System.out.println("容器启动成功: " + factory);
        } catch (Exception e) {
            // 打印异常,但不会影响for循环继续
            System.err.println("容器启动失败: " + factory + ", error: " + e.getMessage());
        }
    });
}

这样,即使某个容器因为 MQ 不可达而长时间卡住,也只会阻塞自己的线程,不会拖慢 for 循环

主线程可以很快完成整个循环,所有容器的启动任务被丢给线程池来异步执行。


5. 原理解释:为什么这样能避免阻塞?

关键点在于线程模型的变化:

  • 原始写法for 循环中的 container.start()同步调用,如果阻塞,主线程就被卡住。
  • 改进写法 :每个 container.start() 都被放入线程池,由线程池里的线程去执行;而主线程仅仅是 submit() 任务,不会等待结果。

因此,阻塞被局限在子线程内,不会影响整个循环的继续执行

这就是"把启动变成了异步"的根本原因。


6. 线程池的销毁问题

由于我们使用了 ExecutorService,需要注意线程池的生命周期:

  • 在容器关闭时,记得调用 executor.shutdown()executor.shutdownNow(),避免线程资源泄漏。
  • 如果想和 Spring 的生命周期绑定,可以考虑用 ThreadPoolTaskExecutor(Spring 管理的线程池),并在容器销毁时调用 executor.destroy()

这样可以保证资源被安全回收。


7. 总结

本文解决的问题是:

  • 问题for 循环批量创建 DMLC 时,如果某个容器启动阻塞,会导致整个循环卡住。
  • 方案:使用线程池,将 DMLC 的启动过程放到异步线程中执行,避免主循环阻塞。
  • 原理:同步 → 异步的线程模型转变,把"坏容器"的阻塞控制在自己线程内,不影响其他容器。
  • 注意点:线程池需要在程序停止时销毁,避免资源泄漏。

最终效果是:即使部分 MQ 不可用,也不会影响其他 MQ 的监听容器正常启动,保证系统整体的健壮性。

相关推荐
野生技术架构师2 分钟前
Spring Boot 定时任务与 xxl-job 灵活切换方案
java·spring boot·后端
寒士obj2 小时前
SpringBoot中的条件注解
java·spring boot·后端
G探险者2 小时前
循环中的阻塞风险与异步线程解法
后端
易元2 小时前
模式组合应用-桥接模式(二)
后端
三婶儿2 小时前
在没有客户端的客户环境下,如何用 Python 一键执行 MySQL 与达梦数据库 SQL
运维·后端·python
G探险者4 小时前
Java 线程相关的三个常见接口、类
后端
学历真的很重要4 小时前
Eino 开源框架全景解析 - 以“大模型应用的搭积木指南”方式理解(一)
后端·语言模型·面试·golang·ai编程·eino
1点东西4 小时前
新来的同事问我当进程/机器突然停止时,finally 到底会不会执行?
java·后端·程序员
AAA修煤气灶刘哥4 小时前
后端仔狂喜!手把手教你用 Java 拿捏华为云 IoTDA,设备上报数据 so easy
后端·物联网·华为