前言
线程池是 Java 中处理多线程的强大工具,但它不仅仅是“直接用就完事”的工具。
很多小伙伴在用线程池时,因为配置不当或忽略细节,踩过许多坑。
今天跟大家一起聊聊线程池中容易踩的 10 个坑,以及如何避免这些坑,希望对你会有所帮助。
1. 直接使用 Executors 创建线程池
许多初学者在创建线程池时,直接使用 Executors 提供的快捷方法:
问题在哪?
- 无界队列:newFixedThreadPool 使用的队列是 LinkedBlockingQueue,它是无界队列,任务堆积可能会导致内存溢出。
- 线程无限增长:newCachedThreadPool 会无限创建线程,在任务量激增时可能耗尽系统资源。
示例:内存溢出的风险
任务数远大于线程数,导致任务无限堆积在队列中,最终可能导致 OutOfMemoryError。
解决办法
使用 ThreadPoolExecutor,并明确指定参数:
2. 错误配置线程数
很多人随意配置线程池参数,比如核心线程数 10,最大线程数 100,看起来没问题,但这可能导致性能问题或资源浪费。
示例:错误配置导致的线程过载
这种配置在任务激增时,会创建大量线程,系统资源被耗尽。
正确配置方式
根据任务类型选择合理的线程数:
- CPU 密集型:线程数建议设置为 CPU 核心数 + 1。
- IO 密集型:线程数建议设置为 2 * CPU 核心数。
示例:
3. 忽略任务队列的选择
任务队列直接影响线程池的行为。如果选错队列类型,会带来很多隐患。
常见队列的坑
- 无界队列:任务无限堆积。
- 有界队列:队列满了会触发拒绝策略。
- 优先级队列:容易导致高优先级任务频繁抢占低优先级任务。
示例:任务堆积导致问题
改进方法:用有界队列,避免任务无限堆积。
4. 忘记关闭线程池
有些小伙伴用完线程池后,忘记调用 shutdown(),导致程序无法正常退出。
示例:线程池未关闭
正确关闭方式
5. 忽略拒绝策略
当任务队列满时,线程池会触发拒绝策略,很多人不知道默认策略(AbortPolicy)会直接抛异常。
示例:任务被拒绝
执行到第四个任务时会抛出 RejectedExecutionException。
改进:选择合适的策略
- CallerRunsPolicy:提交任务的线程自己执行。
- DiscardPolicy:直接丢弃新任务。
- DiscardOldestPolicy:丢弃最老的任务。
6. 任务中未处理异常
线程池中的任务抛出异常时,线程池不会直接抛出,导致很多问题被忽略。
示例:异常被忽略
解决方法
捕获任务内部异常:
自定义线程工厂:
7. 阻塞任务占用线程池
如果线程池中的任务是阻塞的(如文件读写、网络请求),核心线程会被占满,影响性能。
示例:阻塞任务拖垮线程池
改进方法
- 减少任务的阻塞时间。
- 增加核心线程数。
- 使用异步非阻塞方式(如 NIO)。
8. 滥用线程池
线程池不是万能的,某些场景直接使用 new Thread() 更简单。
示例:过度使用线程池
一个简单的短期任务:
这种情况下,用线程池反而复杂。
改进方式
9. 未监控线程池状态
很多人用线程池后,不监控其状态,导致任务堆积、线程耗尽的问题被忽略。
示例:监控线程池状态
结合监控工具(如 JMX、Prometheus),实现实时监控。
10. 动态调整线程池参数
有些人在线程池设计时忽略了参数调整的必要性,导致后期性能优化困难。
示例:动态调整核心线程数
实时调整线程池参数,能适应业务的动态变化。
总结
线程池是强大的工具,但如果我们日常工作中用得不好也非常容易踩坑。
这篇文章通过实际代码示例,我们可以清楚看到线程池的问题所在及改进方法。
希望这些内容能帮你避免踩坑,写出高质量的线程池代码!
线程池用得好,效率杠杠的;用得不好,程序天天崩!