一、问题现象
系统突然出现:
text
java.lang.OutOfMemoryError: unable to create native thread
并伴随:
text
pool-3-thread-1375
pool-3-thread-1383
pool-3-thread-1385
等大量线程。
同时 MQTT 连接异常:
text
Client is not connected (32104)
二、问题分析过程
1. ScheduledExecutorService 是什么?
业务代码:
java
private final ScheduledExecutorService scheduler1 =
Executors.newScheduledThreadPool(4);
private final ScheduledExecutorService scheduler =
Executors.newSingleThreadScheduledExecutor();
分析:
newScheduledThreadPool(4)
表示:
- 创建一个支持"定时任务"的线程池
- 核心线程数 = 4
- 最多可以同时运行4个任务
适用于:
- 定时任务
- 延迟任务
- 周期任务
newSingleThreadScheduledExecutor()
表示:
- 创建一个只有1个线程的定时线程池
- 所有任务串行执行
适用于:
- 顺序执行
- 单线程定时控制
三、为什么线程会爆炸?
用户原有线程池:
java
private static final ThreadPoolExecutor THREAD_EXECUTOR =
new ThreadPoolExecutor(
ThreadConfig.CORE_POOL_SIZE,
ThreadConfig.MAX_POOL_SIZE,
ThreadConfig.KEEP_ALIVE_SECONDS,
TimeUnit.SECONDS,
new SynchronousQueue<>(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
配置:
java
CORE_POOL_SIZE = 50
MAX_POOL_SIZE = 200
但系统实际线程数:
text
1300+
用户疑问:
"不是最大200吗?"
四、真正原因
不是线程池不释放
而是:
系统中疯狂创建了新的线程池
jstack 中发现:
text
pool-779-thread-1
pool-1120-thread-1
pool-2301-thread-1
pool-2798-thread-1
这说明:
- JVM 生命周期里
- 至少创建了 2798 个线程池
五、真正问题代码
用户怀疑代码:
java
private final ScheduledExecutorService scheduler =
Executors.newSingleThreadScheduledExecutor();
类:
java
public class Shower
不是 Spring 单例。
意味着:
java
new Shower()
每次都会:
text
创建一个新的 scheduler
而:
java
newSingleThreadScheduledExecutor()
会:
- 创建永久核心线程
- 默认不会自动释放
六、为什么 shutdown 没生效?
业务代码:
java
public void shutdown() {
if (!scheduler.isShutdown()) {
scheduler.shutdown();
if (!scheduler.awaitTermination(60, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
}
}
这个 shutdown 本身没问题。
真正问题:
很多情况下:
java
mqttFuture.get()
阻塞。
导致:
text
后面的 shutdown 根本没机会执行
于是:
scheduler 永久残留。
七、为什么运行6天后才爆?
因为:
不是瞬间泄漏。
而是:
慢性线程池泄漏
例如:
每天 5000 请求。
即使:
只有 0.1% 的任务:
text
没有成功 shutdown
长期运行后:
线程池数量仍然会越来越大。
最终:
text
unable to create native thread
八、jstack 的核心作用
命令:
bash
jstack PID > jstack.log
作用:
- 查看当前线程状态
- 判断线程卡在哪里
- 判断线程池是否泄漏
注意:
jstack 不是历史日志
它只是:
当前瞬间的线程快照
重启后会消失吗?
会。
因为:
- JVM 已重启
- 线程已重新创建
- 之前线程现场不存在了
所以:
出问题时必须立即抓 jstack
九、正确的线程池架构
错误:
java
每个任务 new scheduler
正确:
java
private static final ScheduledExecutorService SCHEDULER =
new ScheduledThreadPoolExecutor(
8,
new ThreadFactoryBuilder()
.setNameFormat("a-%d")
.build()
);
核心思想:
全局共享线程池
而不是:
text
每个任务创建线程池
十、scheduler 的真正职责
scheduler:
本质上:
只是时间调度器
它负责:
- 接收 schedule 任务
- 到时间后触发任务
其余时间:
text
WAITING
属于正常状态。
十一、正确职责拆分
scheduler
只负责:
text
什么时候执行
例如:
java
scheduler.schedule(() -> {
mqttExecutor.submit(() -> {
doBusiness();
});
}, 10, TimeUnit.MINUTES);
MQTT_EXECUTOR
负责:
text
真正业务执行
例如:
- MQTT publish
- Redis
- DB
- HTTP
十二、为什么 scheduler 线程不能阻塞?
用户之前:
java
scheduler.schedule(() -> {
mqttFuture.get();
});
这会导致:
text
scheduler线程被业务阻塞
例如:
100个淋浴同时触发。
8个线程全部:
text
等待 mqttFuture.get()
后续任务只能排队。
十三、推荐最终架构
scheduler
java
newScheduledThreadPool(8)
只负责调度。
MQTT_EXECUTOR
java
16~32线程
负责业务执行。
future
必须:
java
orTimeout()
或者:
java
get(timeout)
避免永久等待。
十四、最佳异步方案
不推荐:
java
future.get()
推荐:
java
future.whenComplete(...)
真正全异步。
例如:
java
taskSupplier.get()
.orTimeout(10, TimeUnit.SECONDS)
.whenComplete((result, ex) -> {
if (ex != null) {
handleFailure();
return;
}
if (Boolean.TRUE.equals(result)) {
handleSuccess();
} else {
handleFailure();
}
});
十五、最终问题根因总结
真正导致:
text
unable to create native thread
的原因:
不是 JVM 不释放线程。
而是:
动态创建 ScheduledExecutorService
部分异常路径没有 shutdown
newSingleThreadScheduledExecutor 核心线程默认永不超时
最终导致: