惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
The Exploit Database - CXSecurity.com
G
Google Developers Blog
爱范儿
爱范儿
Apple Machine Learning Research
Apple Machine Learning Research
博客园 - 叶小钗
C
Check Point Blog
F
Fortinet All Blogs
WordPress大学
WordPress大学
S
SegmentFault 最新的问题
博客园 - 【当耐特】
Jina AI
Jina AI
T
The Blog of Author Tim Ferriss
P
Palo Alto Networks Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
L
LINUX DO - 热门话题
M
MIT News - Artificial intelligence
Vercel News
Vercel News
博客园 - 司徒正美
Recorded Future
Recorded Future
阮一峰的网络日志
阮一峰的网络日志
P
Proofpoint News Feed
P
Privacy & Cybersecurity Law Blog
Webroot Blog
Webroot Blog
博客园_首页
C
CXSECURITY Database RSS Feed - CXSecurity.com
云风的 BLOG
云风的 BLOG
D
DataBreaches.Net
Y
Y Combinator Blog
J
Java Code Geeks
B
Blog
A
About on SuperTechFans
O
OpenAI News
aimingoo的专栏
aimingoo的专栏
T
Tor Project blog
Stack Overflow Blog
Stack Overflow Blog
月光博客
月光博客
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
博客园 - Franky
AWS News Blog
AWS News Blog
GbyAI
GbyAI
Application and Cybersecurity Blog
Application and Cybersecurity Blog
IT之家
IT之家
V
V2EX
量子位
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
大猫的无限游戏
大猫的无限游戏
Help Net Security
Help Net Security
W
WeLiveSecurity
C
Cyber Attacks, Cyber Crime and Cyber Security

博客园 - buguge

从 `int` 到 `Duration`:一个缓存 API 的三次演进教会我的事 【HttpClient最差实践(bad-practice)】开发者在 http 工具方法中直接实例化 HttpClient,然后…… Crypto、Cipher与Password:Java加密开发的三个核心概念 知识VS技能:如何优雅判空? 20260604SR超时问题排查 推敲见文章:从 `try..catch` 看异常日志打印的正确姿势 #解决问题要彻底# 慢SQL治理完成后,如何防止同类问题“死灰复燃”? 从合同甲方是荒谬的“JD”谈起:软件开发不应遗忘的“常识” 别留小尾巴/尽快剪掉小尾巴:从一次“ABA”字段重命名,谈谈“解决问题要彻底” 常见的OOM错误 ( OutOfMemoryError全类型详解) 开发者暴露了一个无需授权访问的裸接口,我问:如果有人暴力请求怎么办? 【SQL性能优化篇】有了!治理慢SQL“WHERE create_time ORDER BY id”的良药---规避“Using filesort”性能杀手 高效查询商户日终余额:一个SQL的优化实践 Hutool 的 `TimedCache` 到期会自动清理吗? ——————hutool cache的"惰性清理"和"定期清理" Fastjson枚举反序列化:当字符串不是枚举常量名时,会发生什么? fastjson-EnumDeserializer类及源码分析 随笔20260309:我们都是围城里的人 `UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only` 异常解析 认识2个单词:goal/target —————— 为什么Maven是 "goal" 而不是 "target"? 聚合系统设计:策略模式(Strategy Pattern)在银行通道对接场景中的应用 这样构建对象,太帅了!—— 阶梯式Builder模式与代码整洁之道 注意!字段数据类型不匹配,这个sql会很慢 还在用ArrayList?用HashSet吧!--性能对比 分页查询还在用create_time去做降序? 从 synchronized 到 ConcurrentHashMap:一个小小的并发控制策略升级优化,证明我还是初级程序员 面试题沟通整理 【研发笔记20260120】值得记录:靠谱程序员的回聘 研发笔记:如何消除长字符串的秘钥数据对RPC负荷、日志量、系统安全所带来的伤害? 未给entity的主属性赋值,Mybatisplus却抛出了type mismatch异常。——————分享一下Mybatisplus主键填充机制 在java中实现c#的int.TryParse方法 灵活用工平台-连续劳务所得税-计算器-工具类,拿走不谢 Apollo场景建议配置指南:充分发挥分布式配置中心优势
最差实践(bad-practice):开发者在方法里直接实例化线程池对象,然后...(应用gg了)
buguge · 2026-07-09 · via 博客园 - buguge

一、问题现场

当需要异步执行一个任务时,开发者随手在方法内部 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,也没有任何资源清理
    }
}

这段代码看起来“简洁又高效”,甚至能正常工作。但它埋下的隐患,足以让整个应用在流量突增时瞬间崩溃。

二、隐藏的致命问题

1. 线程泄漏 —— 最危险的隐患

这是最容易被忽视却后果最严重的问题。方法内创建的线程池如果不显式调用 shutdown(),其核心线程会一直存活Executors.newFixedThreadPool() 创建的是非守护线程),不会被 GC 回收。

想象一下:每次调用 sendNotification(),就创建一个包含 5 个核心线程的线程池。调用 100 次,应用里就有 500 个存活的线程;调用 10000 次,就有 50000 个线程。每个线程默认占用约 1MB 的栈内存,50000 个线程就是 50GB 内存,系统很快会抛出 OutOfMemoryError: unable to create new native thread

一个真实案例:某电商公司的促销活动期间,由于每次请求都创建线程池处理日志,导致应用线程数从正常的 200 暴涨到 12000,最终 JVM 无法创建新线程,服务彻底瘫痪。

2. 线程池参数形同虚设

Executors.newFixedThreadPool(5) 的本意是控制并发数不超过 5。但由于每次调用都新建线程池,这个限制完全失效——每个线程池都有 5 个线程,并发数变成 调用次数 × 5,完全失控。

3. 任务队列堆积,触发拒绝策略

每个新建的线程池都会附带一个无界队列LinkedBlockingQueue),容量为 Integer.MAX_VALUE。如果任务提交速度远快于处理速度,队列会无限膨胀,最终耗尽堆内存。

4. 频繁 GC 压力

每次 new 线程池都会创建 ThreadPoolExecutorBlockingQueueThreadFactoryRejectedExecutionHandler 等数十个对象,外加 5 个 Thread 对象。在高 QPS 场景下,这会频繁触发 Young GC,拖累整体性能。

5. 资源无法统一管理

当需要调整线程池大小、队列容量、拒绝策略时,开发者必须搜遍全项目,修改每一个 new ThreadPoolExecutor() 的地方,极易遗漏。

三、Spring 项目中的变种:@Async 的优雅陷阱

在 Spring 项目中,开发者往往使用 @Async 注解来替代手动创建线程池,写法更加优雅:

@Service
public class UserService {
    @Async
    public void sendEmail(String to) {
        // 异步发送邮件
    }
}

3.1 @Async 与 AsyncConfigurer 的关系

  • @Async触发开关,标注在方法上,告诉 Spring“这个方法需要异步执行”。
  • AsyncConfigurer配置引擎,实现该接口可以自定义异步任务的线程池和异常处理器。

关键点在于:如果只写 @Async 而不做任何线程池配置,Spring 会使用默认的 SimpleAsyncTaskExecutor

3.2 三种做法的风险等级对比

做法 每次调用创建什么 线程生命周期 风险等级
手动 new Thread() 一个线程 执行完即销毁 ⚠️ 中等(创建销毁频繁,有性能开销)
@Async + SimpleAsyncTaskExecutor(默认) 一个线程 执行完即销毁 ⚠️ 中等(同上,但写法更优雅)
手动 new ThreadPoolExecutor 一个线程池(含多个核心线程) 核心线程永久存活,不会被GC 🔥 极高(线程炸弹)

重点区分

  • SimpleAsyncTaskExecutor(默认):每次调用 @Async 方法都会 new Thread(),执行完毕后线程即终止,被 GC 回收。虽然存在线程创建销毁的性能开销,但线程不会累积,风险相对可控。
  • 方法内 new ThreadPoolExecutor:每次调用都创建一个线程池,其核心线程是长期存活的ThreadPoolExecutor 中的 Worker 线程在完成任务后会阻塞等待新任务,而非退出),且线程池对象本身失去引用后,其内部的线程依然存活,不会被 GC 回收。调用 1000 次就永久占用 5000 个线程,这才是真正的“线程炸弹”。

结论:方法内 new ThreadPoolExecutor 的危险程度,远高于 @Async 默认的 SimpleAsyncTaskExecutor。前者造成永久性线程泄漏,后者仅是临时性线程开销

3.3 正确的做法:实现 AsyncConfigurer 接管控制权

@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;
}

四、修复方案:全局线程池单例(非 Spring 环境)

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 却找不到任何线程池配置时,不妨停下来想一想——这行看似无害的代码,会不会让应用在下一轮流量洪峰中猝死?线程池从来都不是方法级的“玩具”,而是系统级的“基础设施”。