专栏名称: 芋道源码
纯 Java 源码分享公众号,目前有「Dubbo」「SpringCloud」「Java 并发」「RocketMQ」「Sharding-JDBC」「MyCAT」「Elastic-Job」「SkyWalking」「Spring」等等
目录
相关文章推荐
Java编程精选  ·  某程序员:37 ... ·  2 天前  
芋道源码  ·  1.5k ... ·  2 天前  
51好读  ›  专栏  ›  芋道源码

线程池 坑中之王 !

芋道源码  · 公众号  · Java  · 2025-01-27 09:21

主要观点总结

本文介绍了在使用线程池时容易遇到的10个坑,包括直接使用Executors创建线程池、错误配置线程数、忽略任务队列的选择、忘记关闭线程池、忽略拒绝策略、任务中未处理异常、阻塞任务占用线程池、滥用线程池、未监控线程池状态以及动态调整线程池参数等问题。文章通过实际代码示例展示了问题所在及改进方法,帮助读者避免踩坑,写出高质量的线程池代码。

关键观点总结

关键观点1: 直接使用Executors创建线程池的问题

无界队列可能导致内存溢出,线程无限增长可能耗尽系统资源。

关键观点2: 错误配置线程数的风险

随意配置线程池参数可能导致性能问题或资源浪费,应根据任务类型选择合理的线程数。

关键观点3: 任务队列选择的重要性

任务队列直接影响线程池的行为,选错队列类型可能带来隐患,如无界队列导致任务堆积,有界队列可能触发拒绝策略。

关键观点4: 忘记关闭线程池的后果

用完线程池后未关闭可能导致程序无法正常退出。

关键观点5: 忽略拒绝策略的问题

当任务队列满时,线程池会触发拒绝策略,很多人不知道默认策略会直接抛异常。

关键观点6: 任务中未处理异常的解决方案

线程池中的任务抛出异常时,需要捕获并处理这些异常,否则可能导致问题被忽略。

关键观点7: 阻塞任务占用线程池的改进方法

如果线程池中的任务是阻塞的,可能会影响性能,可以通过减少阻塞时间、增加核心线程数或使用异步非阻塞方式来解决。

关键观点8: 滥用线程池的弊端

在某些简单场景下,直接使用新的Thread可能更为简单,过度使用线程池可能并不必要。

关键观点9: 监控线程池状态的重要性

通过监控线程池状态可以及时发现并解决问题,如任务堆积、线程耗尽等。

关键观点10: 动态调整线程池参数的必要性

在线程池设计时,根据业务需求动态调整参数,以适应业务的动态变化。


正文

👉 这是一个或许对你有用的社群

🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入芋道快速开发平台知识星球。下面是星球提供的部分资料: 

👉这是一个或许对你有用的开源项目

国产 Star 破 10w+ 的开源项目,前端包括管理后台 + 微信小程序,后端支持单体和微服务架构。

功能涵盖 RBAC 权限、SaaS 多租户、数据权限、商城、支付、工作流、大屏报表、微信公众号、ERPCRMAI 大模型等等功能:

  • Boot 多模块架构:https://gitee.com/zhijiantianya/ruoyi-vue-pro
  • Cloud 微服务架构:https://gitee.com/zhijiantianya/yudao-cloud
  • 视频教程:https://doc.iocoder.cn
【国内首批】支持 JDK 17/21 + SpringBoot 3.3、JDK 8/11 + Spring Boot 2.7 双版本 

来源:苏三说技术


前言

线程池是 Java 中处理多线程的强大工具,但它不仅仅是“直接用就完事”的工具。

很多小伙伴在用线程池时,因为配置不当或忽略细节,踩过许多坑。

今天跟大家一起聊聊线程池中容易踩的 10 个坑,以及如何避免这些坑,希望对你会有所帮助。

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 视频教程:https://doc.iocoder.cn/video/

1. 直接使用 Executors 创建线程池

许多初学者在创建线程池时,直接使用 Executors 提供的快捷方法:

ExecutorService executor = Executors.newFixedThreadPool(10);

问题在哪?

  • 无界队列newFixedThreadPool 使用的队列是 LinkedBlockingQueue,它是无界队列,任务堆积可能会导致内存溢出。
  • 线程无限增长newCachedThreadPool 会无限创建线程,在任务量激增时可能耗尽系统资源。

示例:内存溢出的风险

ExecutorService executor = Executors.newFixedThreadPool(2);
for (int i = 0; i 1000000; i++) {
    executor.submit(() -> {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
}

任务数远大于线程数,导致任务无限堆积在队列中,最终可能导致 OutOfMemoryError

解决办法

使用 ThreadPoolExecutor,并明确指定参数:

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2,
    4,
    60L,
    TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(100), // 有界队列
    new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud
  • 视频教程:https://doc.iocoder.cn/video/

2. 错误配置线程数

很多人随意配置线程池参数,比如核心线程数 10,最大线程数 100,看起来没问题,但这可能导致性能问题或资源浪费。

示例:错误配置导致的线程过载

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    10// 核心线程数
    100// 最大线程数
    60L,
    TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(10)
);

for (int i = 0; i 1000; i++) {
    executor.submit(() -> {
        try {
            Thread.sleep(5000); // 模拟耗时任务
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
}

这种配置在任务激增时,会创建大量线程,系统资源被耗尽。

正确配置方式

根据任务类型选择合理的线程数:

  • CPU 密集型 :线程数建议设置为 CPU 核心数 + 1
  • IO 密集型 :线程数建议设置为 2 * CPU 核心数

示例:

int cpuCores = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    cpuCores + 1,
    cpuCores + 1,
    60L,
    TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(50)
);

3. 忽略任务队列的选择

任务队列直接影响线程池的行为。如果选错队列类型,会带来很多隐患。

常见队列的坑

  • 无界队列 :任务无限堆积。
  • 有界队列 :队列满了会触发拒绝策略。
  • 优先级队列 :容易导致高优先级任务频繁抢占低优先级任务。

示例:任务堆积导致问题

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2,
    4,
    60L,
    TimeUnit.SECONDS,
    new LinkedBlockingQueue<>()
);

for (int i = 0; i 100000; i++) {
    executor.submit(() -> System.out.println(Thread.currentThread().getName()));
}

改进方法 :用有界队列,避免任务无限堆积。

new ArrayBlockingQueue<>(100);

4. 忘记关闭线程池

有些小伙伴用完线程池后,忘记调用 shutdown(),导致程序无法正常退出。

示例:线程池未关闭

ExecutorService executor = Executors.newFixedThreadPool(5);
executor.submit(() -> System.out.println("任务执行中..."));
// 线程池未关闭,程序一直运行

正确关闭方式

executor.shutdown();
try {
    if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
        executor.shutdownNow();
    }
catch (InterruptedException e) {
    executor.shutdownNow();
}

5. 忽略拒绝策略

当任务队列满时,线程池会触发拒绝策略,很多人不知道默认策略(AbortPolicy)会直接抛异常。

示例:任务被拒绝

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    1,
    1,
    60L,
    TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(2),
    new ThreadPoolExecutor.AbortPolicy() // 默认策略
);

for (int i = 0; i 10; i++) {
    executor.submit(() -> System.out.println("任务"));
}

执行到第四个任务时会抛出 RejectedExecutionException

改进:选择合适的策略

  • CallerRunsPolicy:提交任务的线程自己执行。
  • DiscardPolicy:直接丢弃新任务。
  • DiscardOldestPolicy:丢弃最老的任务。

6. 任务中未处理异常

线程池中的任务抛出异常时,线程池不会直接抛出,导致很多问题被忽略。

示例:异常被忽略

executor.submit(() -> {
    throw new RuntimeException("任务异常");
});

解决方法

  1. 捕获任务内部异常:
executor.submit(() -> {
    try {
        throw new RuntimeException("任务异常");
    } catch (Exception e) {
        System.err.println("捕获异常:" + e.getMessage());
    }
});
  1. 自定义线程工厂:
ThreadFactory factory = r -> {
    Thread t = new Thread(r);
    t.setUncaughtExceptionHandler((thread, e) -> {
        System.err.println("线程异常:" + e.getMessage());
    });
    return t;
};

7. 阻塞任务占用线程池

如果线程池中的任务是阻塞的(如文件读写、网络请求),核心线程会被占满,影响性能。

示例:阻塞任务拖垮线程池

executor.submit(() -> {
    Thread.sleep(10000); // 模拟阻塞任务
});

改进方法

  • 减少任务的阻塞时间。
  • 增加核心线程数。
  • 使用异步非阻塞方式(如 NIO)。

8. 滥用线程池

线程池不是万能的,某些场景直接使用 new Thread() 更简单。

示例:过度使用线程池

一个简单的短期任务:

ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> System.out.println("执行任务"));
executor.shutdown();

这种情况下,用线程池反而复杂。

改进方式

new Thread(() -> System.out.println("执行任务")).start();

9. 未监控线程池状态

很多人用线程池后,不监控其状态,导致任务堆积、线程耗尽的问题被忽略。

示例:监控线程池状态

System.out.println("核心线程数:" + executor.getCorePoolSize());
System.out.println("队列大小:" + executor.getQueue().size());
System.out.println("已完成任务数:" + executor.getCompletedTaskCount());

结合监控工具(如 JMX、Prometheus),实现实时监控。

10. 动态调整线程池参数

有些人在线程池设计时忽略了参数调整的必要性,导致后期性能优化困难。

示例:动态调整核心线程数

executor.setCorePoolSize(20);
executor.setMaximumPoolSize(50);

实时调整线程池参数,能适应业务的动态变化。

总结

线程池是强大的工具,但如果我们日常工作中用得不好也非常容易踩坑。

这篇文章通过实际代码示例,我们可以清楚看到线程池的问题所在及改进方法。

希望这些内容能帮你避免踩坑,写出高质量的线程池代码!

线程池用得好,效率杠杠的;用得不好,程序天天崩!


欢迎加入我的知识星球,全面提升技术能力。

👉 加入方式,长按”或“扫描”下方二维码噢

星球的内容包括:项目实战、面试招聘、源码解析、学习路线。

文章有帮助的话,在看,转发吧。

谢谢支持哟 (*^__^*)