大家好,我是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 的监听容器正常启动,保证系统整体的健壮性。