⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 苏三说技术 「苏三」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

前言

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

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

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

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() // 拒绝策略
);

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);

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

总结

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

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

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

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

文章目录
  1. 1. 前言
  2. 2. 1. 直接使用 Executors 创建线程池
    1. 2.0.1. 问题在哪?
    2. 2.0.2. 示例:内存溢出的风险
    3. 2.0.3. 解决办法
  • 3. 2. 错误配置线程数
    1. 3.0.1. 示例:错误配置导致的线程过载
    2. 3.0.2. 正确配置方式
  • 4. 3. 忽略任务队列的选择
    1. 4.0.1. 常见队列的坑
    2. 4.0.2. 示例:任务堆积导致问题
  • 5. 4. 忘记关闭线程池
    1. 5.0.1. 示例:线程池未关闭
    2. 5.0.2. 正确关闭方式
  • 6. 5. 忽略拒绝策略
    1. 6.0.1. 示例:任务被拒绝
    2. 6.0.2. 改进:选择合适的策略
  • 7. 6. 任务中未处理异常
    1. 7.0.1. 示例:异常被忽略
    2. 7.0.2. 解决方法
  • 8. 7. 阻塞任务占用线程池
    1. 8.0.1. 示例:阻塞任务拖垮线程池
    2. 8.0.2. 改进方法
  • 9. 8. 滥用线程池
    1. 9.0.1. 示例:过度使用线程池
    2. 9.0.2. 改进方式
  • 10. 9. 未监控线程池状态
    1. 10.0.1. 示例:监控线程池状态
  • 11. 10. 动态调整线程池参数
    1. 11.0.1. 示例:动态调整核心线程数
  • 12. 总结