





















当需要异步执行一个任务时,开发者随手在方法内部 new 一个线程池:
public class AsyncTaskService {
public void sendNotification(String message) {
// 每次调用都创建全新的线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
executor.submit(() -> {
// 模拟发送通知
System.out.println("发送通知: " + message);
try { Thread.sleep(1000); } catch (InterruptedException e) {}
});
// 注意:这里既没有 shutdown,也没有任何资源清理
}
}
这段代码看起来“简洁又高效”,甚至能正常工作。但它埋下的隐患,足以让整个应用在流量突增时瞬间崩溃。
这是最容易被忽视却后果最严重的问题。方法内创建的线程池如果不显式调用 shutdown(),其核心线程会一直存活(Executors.newFixedThreadPool() 创建的是非守护线程),不会被 GC 回收。
想象一下:每次调用 sendNotification(),就创建一个包含 5 个核心线程的线程池。调用 100 次,应用里就有 500 个存活的线程;调用 10000 次,就有 50000 个线程。每个线程默认占用约 1MB 的栈内存,50000 个线程就是 50GB 内存,系统很快会抛出 OutOfMemoryError: unable to create new native thread。
一个真实案例:某电商公司的促销活动期间,由于每次请求都创建线程池处理日志,导致应用线程数从正常的 200 暴涨到 12000,最终 JVM 无法创建新线程,服务彻底瘫痪。
Executors.newFixedThreadPool(5) 的本意是控制并发数不超过 5。但由于每次调用都新建线程池,这个限制完全失效——每个线程池都有 5 个线程,并发数变成 调用次数 × 5,完全失控。
每个新建的线程池都会附带一个无界队列(LinkedBlockingQueue),容量为 Integer.MAX_VALUE。如果任务提交速度远快于处理速度,队列会无限膨胀,最终耗尽堆内存。
每次 new 线程池都会创建 ThreadPoolExecutor、BlockingQueue、ThreadFactory、RejectedExecutionHandler 等数十个对象,外加 5 个 Thread 对象。在高 QPS 场景下,这会频繁触发 Young GC,拖累整体性能。
当需要调整线程池大小、队列容量、拒绝策略时,开发者必须搜遍全项目,修改每一个 new ThreadPoolExecutor() 的地方,极易遗漏。
在 Spring 项目中,开发者往往使用 @Async 注解来替代手动创建线程池,写法更加优雅:
@Service
public class UserService {
@Async
public void sendEmail(String to) {
// 异步发送邮件
}
}
@Async 是触发开关,标注在方法上,告诉 Spring“这个方法需要异步执行”。AsyncConfigurer 是配置引擎,实现该接口可以自定义异步任务的线程池和异常处理器。关键点在于:如果只写 @Async 而不做任何线程池配置,Spring 会使用默认的 SimpleAsyncTaskExecutor。
| 做法 | 每次调用创建什么 | 线程生命周期 | 风险等级 |
|---|---|---|---|
手动 new Thread() |
一个线程 | 执行完即销毁 | ⚠️ 中等(创建销毁频繁,有性能开销) |
| @Async + SimpleAsyncTaskExecutor(默认) | 一个线程 | 执行完即销毁 | ⚠️ 中等(同上,但写法更优雅) |
手动 new ThreadPoolExecutor |
一个线程池(含多个核心线程) | 核心线程永久存活,不会被GC | 🔥 极高(线程炸弹) |
重点区分:
SimpleAsyncTaskExecutor(默认):每次调用 @Async 方法都会 new Thread(),执行完毕后线程即终止,被 GC 回收。虽然存在线程创建销毁的性能开销,但线程不会累积,风险相对可控。new ThreadPoolExecutor:每次调用都创建一个线程池,其核心线程是长期存活的(ThreadPoolExecutor 中的 Worker 线程在完成任务后会阻塞等待新任务,而非退出),且线程池对象本身失去引用后,其内部的线程依然存活,不会被 GC 回收。调用 1000 次就永久占用 5000 个线程,这才是真正的“线程炸弹”。结论:方法内
new ThreadPoolExecutor的危险程度,远高于@Async默认的SimpleAsyncTaskExecutor。前者造成永久性线程泄漏,后者仅是临时性线程开销。
@Configuration
@EnableAsync
public class CustomAsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
// 手动配置一个安全的线程池,替代默认的 SimpleAsyncTaskExecutor
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(500); // 有界队列,防止内存溢出
executor.setThreadNamePrefix("Async-Executor-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
// 处理 @Async 方法抛出的未捕获异常(可选)
return (ex, method, params) ->
log.error("异步方法执行异常: {}", method.getName(), ex);
}
}
提示:如果不实现
AsyncConfigurer,也可以通过声明一个名为taskExecutor的 Bean 达到同样效果:
@Bean(name = "taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// ... 参数配置 ...
return executor;
}
public class ThreadPoolManager {
// 核心线程数:通常设置为 CPU 核心数 + 1
private static final int CORE_POOL_SIZE = Runtime.getRuntime().availableProcessors() + 1;
// 最大线程数:视 I/O 密集型还是 CPU 密集型而定
private static final int MAX_POOL_SIZE = Runtime.getRuntime().availableProcessors() * 2;
// 空闲存活时间
private static final long KEEP_ALIVE_TIME = 60L;
// 有界队列,防止内存溢出
private static final int QUEUE_CAPACITY = 1000;
private static final ThreadPoolExecutor EXECUTOR = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(QUEUE_CAPACITY),
new ThreadFactoryBuilder().setNameFormat("async-pool-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy() // 队列满时由调用者线程执行,防止任务丢失
);
static {
// 注册 JVM 关闭钩子,确保应用退出时优雅关闭线程池
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
EXECUTOR.shutdown();
try {
if (!EXECUTOR.awaitTermination(30, TimeUnit.SECONDS)) {
EXECUTOR.shutdownNow();
if (!EXECUTOR.awaitTermination(30, TimeUnit.SECONDS)) {
System.err.println("线程池未能完全终止");
}
}
} catch (InterruptedException e) {
EXECUTOR.shutdownNow();
Thread.currentThread().interrupt();
}
}));
}
private ThreadPoolManager() {}
public static void execute(Runnable task) {
EXECUTOR.execute(task);
}
public static <T> Future<T> submit(Callable<T> task) {
return EXECUTOR.submit(task);
}
// 监控接口:获取当前活跃线程数、队列大小等
public static String getPoolStatus() {
return String.format("活跃线程: %d, 队列积压: %d, 已完成任务: %d",
EXECUTOR.getActiveCount(),
EXECUTOR.getQueue().size(),
EXECUTOR.getCompletedTaskCount());
}
}
使用方式:
public class AsyncTaskService {
public void sendNotification(String message) {
ThreadPoolManager.execute(() -> {
// 异步执行任务
System.out.println("发送通知: " + message);
});
}
}
| 改进项 | 说明 |
|---|---|
| 单例化 | 整个应用生命周期内只存在一个线程池,线程数可控 |
| 有界队列 | 防止任务无限堆积,触发拒绝策略后由调用者线程执行,确保任务不丢失 |
| 统一配置 | 核心/最大线程数、队列容量、拒绝策略全局可控,调优只需改一处 |
| 优雅关闭 | 注册 JVM ShutdownHook,确保应用退出时等待任务完成或强制终止 |
| 可观测性 | 提供 getPoolStatus() 方法,便于监控线程池运行状态 |
如果系统中有多种类型的异步任务,可以按业务类型隔离线程池,避免相互影响:
public class ThreadPoolManager {
// 通知类任务:I/O 密集型,线程数可以大一些
public static final ThreadPoolExecutor NOTIFICATION_POOL = new ThreadPoolExecutor(
10, 50, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(500),
new ThreadFactoryBuilder().setNameFormat("notification-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
// 日志类任务:CPU 密集型,线程数不宜过多
public static final ThreadPoolExecutor LOG_POOL = new ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors(),
Runtime.getRuntime().availableProcessors() + 1,
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(2000),
new ThreadFactoryBuilder().setNameFormat("log-%d").build(),
new ThreadPoolExecutor.DiscardPolicy() // 日志允许丢弃
);
// 为每个线程池注册 ShutdownHook...
}
线程池数配置原则:
- CPU 密集型任务(计算为主):线程数 = CPU 核心数 + 1
- I/O 密集型任务(网络/磁盘等待为主):线程数 = CPU 核心数 × 2(甚至更高)
| 错误做法 | 正确做法 |
|---|---|
每次方法调用都 new ThreadPoolExecutor() |
应用全局只创建一份线程池实例 |
使用 Executors.newFixedThreadPool() 等快捷方法 |
使用 ThreadPoolExecutor 构造方法并显式指定有界队列 |
| 不关心线程池参数 | 根据业务类型合理配置核心线程数、最大线程数、队列容量 |
不调用 shutdown() |
注册 JVM ShutdownHook 或使用 Spring @PreDestroy 优雅关闭 |
| 将线程池视为轻量对象 | 视作重量级资源,全局复用 |
在 Spring 中只写 @Async,不配置线程池 |
实现 AsyncConfigurer 或显式定义 taskExecutor Bean |
一句话记住:ThreadPoolExecutor 是线程安全的重量级执行引擎,应当全局单例、合理配置、优雅关闭。
在 Spring 中,@Async 注解只是“需求方”,必须通过 AsyncConfigurer 或显式定义 taskExecutor Bean 来提供安全的“资源供给方”,否则它将退化为每次新建线程的 SimpleAsyncTaskExecutor——虽然比“方法内 new ThreadPoolExecutor”的线程泄漏风险低,但仍然存在线程创建销毁频繁的性能开销,失去了线程池复用的核心优势。
下次你在方法里看到
new ThreadPoolExecutor(...)或者只写@Async却找不到任何线程池配置时,不妨停下来想一想——这行看似无害的代码,会不会让应用在下一轮流量洪峰中猝死?线程池从来都不是方法级的“玩具”,而是系统级的“基础设施”。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。