如何在批量创建 `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 的监听容器正常启动,保证系统整体的健壮性。

相关推荐
学长毕设1 小时前
基于SpringBoot的野生动物公益保护平台设计与实现-附源码
java·spring boot·后端
梦里寻码2 小时前
我用 NestJS + Vue3 写了一套完整的研发管理系统(58 表 / 229 API / 130 项断言,全开源
前端·后端·算法
鶴哥只手遮天2 小时前
《Redis 8.10.3 深度剖析》系列 ·第 10 章 Hash 一:`listpack`、`hashtable` 与 `LISTPACK_EX`
redis·后端·开源
johnjincaaaa2 小时前
WebAssembly 逆向实战:从 0 搞懂猿人学第 15 题
后端
Gopher_HBo2 小时前
Go CPU 剖析:四角色白话机制 + 实测实验
后端
猿人谷2 小时前
Surrogate Model:让进化算法学会“预测”,为什么它正在成为智能优化的重要技术?
后端·算法·架构
the局外人3 小时前
LangChain ToolRuntime 入门:context、state 与 store 的实际用法
后端·langchain·agent
回家路上绕了弯3 小时前
当 AI 越来越能干,我们如何理解自己的价值?
后端
陈随易3 小时前
Node.js 2026上半年都有哪些性能提升?
前端·后端·程序员
hhcgchpspk3 小时前
flask服务端页面预防被爬措施
后端·python·flask·网络爬虫·防爬