由于不知道Java线程池的bug,某程序员叕被祭天(下)

简介: 由于不知道Java线程池的bug,某程序员叕被祭天

通过ThreadPoolExecutor#execute()提交任务时,若任务在执行的过程中出现运行时异常,会导致 执行任务的线程 终止。

但要命的是,有时任务虽然异常了,但你却收不到任何通知,你还在开心摸鱼,以为任务都执行很正常。虽然线程池提供了很多用于异常处理的方法,但最稳妥和简单的方案还是捕获所有异常并具体处理:

image.png

线程池的线程管理


还好有谷歌,一般我们直接利用guava的ThreadFactoryBuilder实现线程池线程的自定义命名即可。


线程池的拒绝策略


线程池默认的拒绝策略会抛RejectedExecutionException,这是个运行时异常,IDEA不会强制捕获,所以我们也很容易忽略它。

对于采用何种策略,具体要看任务的重要性:


  • 若是一些不重要任务,可选择直接丢弃
  • 重要任务,可采用降级,比如将任务信息插入DB或MQ,启用一个专门用作补偿的线程池去补偿处理。所谓降级,也就是在服务无法正常提供功能的情况下,采取的补救措施。具体处理方式也看具体场景而不同。
  • 当线程数大于核心线程数时,线程等待keepAliveTime后还是无任务需要处理,收缩线程到核心线程数

了解这个策略,有助于我们根据实际的容量规划需求,为线程池设置合适的初始化参数。也可通过一些手段来改变这些默认工作行为,比如:

  • 声明线程池后立即调用prestartAllCoreThreads,启动所有核心线程

image.png

  • 传true给allowCoreThreadTimeOut,让线程池在空闲时同样回收核心线程

image.png

弹性伸缩的实现


线程池是先用Q存放来不及处理的任务,满后再扩容线程池。当Q设置很大时(那个 工具类),最大线程数这个参数就没啥意义了,因为队列很难满或到满时可能已OOM,更没机会去扩容线程池了。

是否能让线程池优先开启更多线程,而把Q当成后续方案?比如我们的任务执行很慢,需要10s,若线程池可优先扩容到5个最大线程,那么这些任务最终都可以完成,而不会因为线程池扩容过晚导致慢任务来不及处理。


难题在于:

  • 线程池在工作队列满时,会扩容线程池
    重写队列的offer,人为制造该队列满的条件
  • 改变了队列机制,达到最大线程后势必要触发拒绝策略
    实现一个自定义拒绝策略,这时再把任务真正插入队列


Tomcat就实现了类似的“弹性”线程池。


务必确认清楚线程池本身是不是复用的


某服务偶尔报警线程数过多,但过一会儿又会降下来,但应用的请求量却变化不大。

可以在线程数较高时抓取线程栈,发现内存中有上千个线程池,这肯定不正常!

但代码里也没看到声明了线程池,最后发现原来是业务代码调用了一个类库:

image.png

该类库竟然每次都创建一个新的线程池!

image.png

newCachedThreadPool会在需要时创建必要数量的线程,业务代码的一次业务操作会向线程池提交多个慢任务,这样执行一次业务操作就会开启多个线程。如果业务操作并发量较大的话,的确有可能一下子开启几千个线程。


那为何监控中看到线程数量会下降,而不OOM?


newCachedThreadPool的核心线程数是0,而keepAliveTime是60s,所以60s后所有线程都可回收。


那这如何修复呢?


使用static字段存放线程池引用即可

image.png

线程池的意义在于复用,就意味着程序应该始终使用一个线程池吗?


不,具体场景具体分析。


比如一个 I/O 型任务,不断向线程池提交任务:向一个文件写入大量数据。线程池的线程基本一直处于忙碌状态,队列也基本满。而且由于是CallerRunsPolicy策略,所以当线程满队列满,任务会在提交任务的线程或调用execute方法的线程执行,所以不要认为提交到线程池的任务就一定会被异步处理。


毕竟,若使用CallerRunsPolicy,就有可能异步任务变同步执行。使用CallerRunsPolicy,当线程池饱和时,计算任务会在执行Web请求的Tomcat线程执行,这时就会进一步影响到其他同步处理的线程,甚至造成整个应用程序崩溃。


如何修正?


使用单独的线程池处理这种“I/O型任务”,将线程数设置多一些!

所以千万不要盲目复用别人写的线程池!因为它不一定适合你的任务!


Java 8的parallel stream


可方便并行处理集合中的元素,共享同一ForkJoinPool,默认并行度是CPU核数-1。对于CPU绑定的任务,使用这样的配置较合适,但若集合操作涉及同步I/O操作(比如数据库操作、外部服务调用),建议自定义一个ForkJoinPool(或普通线程池)。


最后声明一点:提交到相同线程池中的任务,一定要是相互独立的,最好不要有依赖关系!



参考

  • 《阿里巴巴Java开发手册》
目录
相关文章
|
10天前
|
安全 Java 测试技术
Java并行流陷阱:为什么指定线程池可能是个坏主意
本文探讨了Java并行流的使用陷阱,尤其是指定线程池的问题。文章分析了并行流的设计思想,指出了指定线程池的弊端,并提供了使用CompletableFuture等替代方案。同时,介绍了Parallel Collector库在处理阻塞任务时的优势和特点。
|
21天前
|
Java 程序员
JAVA程序员的进阶之路:掌握URL与URLConnection,轻松玩转网络资源!
在Java编程中,网络资源的获取与处理至关重要。本文介绍了如何使用URL与URLConnection高效、准确地获取网络资源。首先,通过`java.net.URL`类定位网络资源;其次,利用`URLConnection`类实现资源的读取与写入。文章还提供了最佳实践,包括异常处理、连接池、超时设置和请求头与响应头的合理配置,帮助Java程序员提升技能,应对复杂网络编程场景。
47 9
|
3月前
|
存储 算法 Java
惊!Java程序员必看:JVM调优揭秘,堆溢出、栈溢出如何巧妙化解?
【8月更文挑战第29天】在Java领域,JVM是代码运行的基础,但需适当调优以发挥最佳性能。本文探讨了JVM中常见的堆溢出和栈溢出问题及其解决方法。堆溢出发生在堆空间不足时,可通过增加堆空间、优化代码及释放对象解决;栈溢出则因递归调用过深或线程过多引起,调整栈大小、优化算法和使用线程池可有效应对。通过合理配置和调优JVM,可确保Java应用稳定高效运行。
140 4
|
3月前
|
Java 调度 数据库
Java并发编程:深入理解线程池
在Java并发编程的海洋中,线程池是一艘强大的船,它不仅提高了性能,还简化了代码结构。本文将带你潜入线程池的深海,探索其核心组件、工作原理及如何高效利用线程池来优化你的并发应用。
|
3月前
|
存储 监控 Java
Java多线程优化:提高线程池性能的技巧与实践
Java多线程优化:提高线程池性能的技巧与实践
115 1
|
3月前
|
Java 程序员
Java数据类型:为什么程序员都爱它?
Java数据类型:为什么程序员都爱它?
53 1
|
22天前
|
监控 安全 Java
在 Java 中使用线程池监控以及动态调整线程池时需要注意什么?
【10月更文挑战第22天】在进行线程池的监控和动态调整时,要综合考虑多方面的因素,谨慎操作,以确保线程池能够高效、稳定地运行,满足业务的需求。
100 38
|
6天前
|
SQL 存储 Java
面向 Java 程序员的 SQLite 替代品
SQLite 是轻量级数据库,适用于小微型应用,但其对外部数据源支持较弱、无存储过程等问题影响了开发效率。esProc SPL 是一个纯 Java 开发的免费开源工具,支持标准 JDBC 接口,提供丰富的数据源访问、强大的流程控制和高效的数据处理能力,尤其适合 Java 和安卓开发。SPL 代码简洁易懂,支持热切换,可大幅提高开发效率。
|
18天前
|
SQL Java 程序员
倍增 Java 程序员的开发效率
应用计算困境:Java 作为主流开发语言,在数据处理方面存在复杂度高的问题,而 SQL 虽然简洁但受限于数据库架构。SPL(Structured Process Language)是一种纯 Java 开发的数据处理语言,结合了 Java 的架构灵活性和 SQL 的简洁性。SPL 提供简洁的语法、完善的计算能力、高效的 IDE、大数据支持、与 Java 应用无缝集成以及开放性和热切换特性,能够大幅提升开发效率和性能。
|
22天前
|
Prometheus 监控 Cloud Native
JAVA线程池监控以及动态调整线程池
【10月更文挑战第22天】在 Java 中,线程池的监控和动态调整是非常重要的,它可以帮助我们更好地管理系统资源,提高应用的性能和稳定性。
59 4

热门文章

最新文章