五个Java中线程池使用不当的避坑指南
线程池是 Java 多线程编程中的一个重要概念,它可以有效地管理和复用线程资源,提高系统的性能和稳定性。但是线程池的使用也有一些注意事项和常见的错误,如果不小心,就可能会导致一些严重的问题,比如内存泄漏、死锁、性能下降等。
本文将介绍线程池使用不当的五个坑,以及如何避免和解决它们,大纲如下,
坑一:线程池中异常消失
线程池执行方法时要添加异常处理,这是一个老生常谈的问题,可是直到最近我都有同事还在犯这个错误,所以我还是要讲一下,不过我还提到了一种优雅的线程池全局异常处理的方法,大家可以往下看。
问题原因
@Test public void test() throws Exception { ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( 5, 10, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100000)); Future<Integer> submit = threadPoolExecutor.execute(() -> { int i = 1 / 0; // 发生异常 return i; }); }
如上代码,在线程池执行任务时,没有添加异常处理。导致任务内部发生异常时,内部错误无法被记录下来。
解决方法
在线程池执行任务方法内添加 try/catch 处理,代码如下,
@Test public void test() throws Exception { ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( 5, 10, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100000)); Future<Integer> submit = threadPoolExecutor.execute(() -> { try { int i = 1 / 0; return i; } catch (Exception e) { log.error(e.getMessage(), e); return null; } }); }
优雅的进行线程池异常处理
当线程池调用任务方法很多时,那么每个线程池任务执行的方法内都要添加 try/catch 处理,这就不优雅了,其实 ThreadPoolExecutor 线程池类支持传入 ThreadFactory 参数用于自定义线程工厂,这样我们在创建线程时,就可以指定 setUncaughtExceptionHandler 异常处理方法。
这样就可以做到全局处理异常了,代码如下,
ThreadFactory threadFactory = r -> { Thread thread = new Thread(r); thread.setUncaughtExceptionHandler((t, e) -> { // 记录线程异常 log.error(e.getMessage(), e); }); return thread; }; ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( 5, 10, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100000)); threadPoolExecutor.execute(() -> { log.info("---------------------"); int i = 1 / 0; });
不过要注意的是上面 setUncaughtExceptionHandler 方法只能针对线程池的 execute 方法来全局处理异常。对于线程池的 submit 方法是无法处理的。
坑二:拒绝策略设置错误导致接口超时
在 Java 中,线程池拒绝策略可以说一个常见八股文问题。大家虽然都记住了线程池有四种决绝策略,可是实际代码编写中,我发现大多数人都只会用 CallerRunsPolicy 策略(由调用线程处理任务)。我吃过这个亏,因此也拿出来讲讲。
问题原因
曾经有一个线上业务接口使用了线程池进行第三方接口调用,线程池配置里的拒绝策略采用的是 CallerRunsPolicy。示例代码如下,
// 某个线上线程池配置如下 ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( 50, // 最小核心线程数 50, // 最大线程数,当队列满时,能创建的最大线程数 60L, TimeUnit.SECONDS, // 空闲线程超过核心线程时,回收该线程的最大等待时间 new LinkedBlockingQueue<>(5000), // 阻塞队列大小,当核心线程使用满时,新的线程会放进队列 new CustomizableThreadFactory("task"), // 自定义线程名 new ThreadPoolExecutor.CallerRunsPolicy() // 线程执行的拒绝策略 ); threadPoolExecutor.execute(() -> { // 调用第三方接口 ... });
在第三方接口异常的情况下,线程池任务调用第三方接口一直超时,导致核心线程数、最大线程数堆积被占满、阻塞队列也被占满的情况下,也就会执行拒绝策略,但是由于使用的是 CallerRunsPolicy 策略,导致线程任务直接由我们的业务线程来执行。
因为第三方接口异常,所以业务线程执行也会继继续超时,线上服务采用的 Tomcat 容器,最终也就导致 Tomcat 的最大线程数也被占满,进而无法继续向外提供服务。
解决方法
首先我们要考虑业务接口的可用性,就算线程池任务被丢弃,也不应该影响业务接口。
在业务接口稳定性得到保证的情况下,在考虑到线程池任务的重要性,不是很重要的话,可以使用 DiscardPolicy 策略直接丢弃,要是很重要,可以考虑使用消息队列来替换线程池。
坑三:重复创建线程池导致内存溢出
不知道大家有没有犯过这个问题,不过我确实犯过,归根结底还是写代码前,没有思考好业务逻辑,直接动手,写一步算一步 。所以说写代码的前的一些逻辑梳理、拆分、代码设计很重要。
问题原因
这个问题的原因很简单,就是在一个方法内重复创建了线程池,在执行完之后却没有关闭。比较经典的就是在定时任务内使用线程池时有可能犯这个问题,示例代码如下,
@XxlJob("test") public void test() throws Exception { // 某个线上线程池配置如下 ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( 50, // 最小核心线程数 50, // 最大线程数,当队列满时,能创建的最大线程数 60L, TimeUnit.SECONDS, // 空闲线程超过核心线程时,回收该线程的最大等待时间 new LinkedBlockingQueue<>(5000), // 阻塞队列大小,当核心线程使用满时,新的线程会放进队列 new CustomizableThreadFactory("task"), // 自定义线程名 new ThreadPoolExecutor.CallerRunsPolicy() // 线程执行的拒绝策略 ); threadPoolExecutor.execute(() -> { // 任务逻辑 ... }); }
当我们在定时任务中想使用线程池来缩短任务执行时间时,千万要注意别再任务内创建了线程池,一旦犯了,基本都会在程序运行一段时间后发现程序突然间就挂了,留下了一堆内存 dump 报错的文件。
解决方法
使用线程池单例,切勿重复创建线程池。示例代码如下,
// 某个线上线程池配置如下 ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( 50, // 最小核心线程数 50, // 最大线程数,当队列满时,能创建的最大线程数 60L, TimeUnit.SECONDS, // 空闲线程超过核心线程时,回收该线程的最大等待时间 new LinkedBlockingQueue<>(5000), // 阻塞队列大小,当核心线程使用满时,新的线程会放进队列 new CustomizableThreadFactory("task"), // 自定义线程名 new ThreadPoolExecutor.CallerRunsPolicy() // 线程执行的拒绝策略 ); @XxlJob("test") public void test() throws Exception { threadPoolExecutor.execute(() -> { // 任务逻辑 // ... }); }
坑四:共用线程池执行不同类型任务导致效率低下
有时候,我们可能会想要节省线程资源,把不同类型的任务都放到同一个线程池中执行,比如主要的业务逻辑和次要的日志记录、监控等。这看起来很合理,但是实际上,这样做可能会导致一个任务影响另一个任务,甚至导致死锁的问题。
问题原因
问题的原因是,不同类型的任务可能有不同的执行时间、优先级、依赖关系等,如果放到同一个线程池中,就可能会出现以下几种情况:
- 如果一个任务执行时间过长,或者出现异常,那么它就会占用线程池中的一个线程,导致其他任务无法及时得到执行,影响系统的吞吐量和响应时间。
- 如果一个任务的优先级较低,或者不是很重要,那么它就可能抢占线程池中的一个线程,导致其他任务无法及时得到执行,影响系统的可用性和正确性。
- 如果一个任务依赖于另一个任务的结果,或者需要等待另一个任务的完成,那么它就可能造成线程池中的一个线程被阻塞,导致其他任务无法及时得到执行,甚至导致死锁的问题。
解决方法
解决方法也很简单,就是使用不同的线程池来执行不同类型的任务,根据任务的特点和重要性来分配线程资源,避免一个任务影响另一个任务。具体来说,有以下几个建议:
- 对于主要的业务逻辑,使用一个专门的线程池,根据业务的并发度和响应时间,设置合适的线程池参数,保证业务的正常运行和高效处理。
- 对于次要的日志记录、监控等,使用一个单独的线程池,根据任务的频率和重要性,设置合适的线程池参数,保证任务的异步执行和不影响主业务。
- 对于有依赖关系的任务,使用一个单独的线程池,根据任务的数量和复杂度,设置合适的线程池参数,保证任务的有序执行和不造成死锁。
坑五:使用 ThreadLocal 和线程池的不兼容问题
ThreadLocal 是 Java 提供的一个工具类,它可以让每个线程拥有自己的变量副本,从而实现线程间的数据隔离,比如存储一些线程相关的上下文信息,如用户 ID、请求 ID 等。这看起来很有用,但是如果和线程池一起使用,就可能会出现一些意想不到的问题,比如数据错乱、内存泄漏等。
问题原因
问题的原因是,ThreadLocal 和线程池的设计理念是相悖的,ThreadLocal 是基于线程的,而线程池是基于任务的。具体来说,有以下几个问题:
- ThreadLocal 的变量是绑定在线程上的,而线程池的线程是可以复用的,如果一个线程执行完一个任务后,没有清理 ThreadLocal 的变量,那么这个变量就会被下一个执行的任务继承,导致数据错乱的问题。
- ThreadLocal 的变量是存储在 Thread 类的一个 ThreadLocalMap 类型的属性中的,这个属性是一个弱引用的 Map,它的键是 ThreadLocal 对象,而值是变量的副本。如果 ThreadLocal 对象被回收,那么它的键就会失效,但是值还会保留在 Map 中,导致内存泄漏的问题。
解决方法
解决方法也很简单,就是在使用 ThreadLocal 和线程池的时候,注意以下几点:
- 在使用 ThreadLocal 的变量之前,要确保为每个线程设置了正确的初始值,避免使用上一个任务的遗留值。
- 在使用 ThreadLocal 的变量之后,要及时地清理 ThreadLocal 的变量,避免变量的副本被下一个执行的任务继承,或者占用内存空间,导致内存泄漏的问题。可以使用 try-finally 语句,或者使用 Java 8 提供的 AutoCloseable 接口,来实现自动清理的功能。
- 在使用 ThreadLocal 的时候,要注意线程池的大小和任务的数量,避免创建过多的 ThreadLocal 对象和变量的副本,导致内存占用过大的问题。可以使用一些工具,如 VisualVM,来监控线程池和 ThreadLocal 的状态,及时发现和解决问题。
总结
本文给大家介绍了线程池使用不当的五个坑,分别是线程池中异常消失、线程池决绝策略设置错误、重复创建线程池导致内存溢出、使用同一个线程池执行不同类型的任务、使用 ThreadLocal 和线程池的不兼容问题,以及它们的问题原因和解决方法。
到此这篇关于五个Java中线程池使用不当的避坑指南的文章就介绍到这了,更多相关Java线程池内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
play for scala 实现SessionFilter 过滤未登录用户跳转到登录页面
这篇文章主要介绍了play for scala 实现SessionFilter 过滤未登录用户跳转到登录页面的相关资料,需要的朋友可以参考下2016-11-11SpringBoot Admin 如何实现Actuator端点可视化监控
这篇文章主要介绍了SpringBoot Admin 如何实现Actuator端点可视化监控,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教2021-08-08SpringBoot 使用 @Value 注解读取配置文件给静态变量赋值
这篇文章主要介绍了SpringBoot 使用 @Value 注解读取配置文件给静态变量赋值,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧2020-11-11SpringBean和Controller实现动态注册与注销过程详细讲解
这篇文章主要介绍了SpringBean和Controller实现动态注册与注销过程,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习吧2023-02-02VSCode中开发JavaWeb项目的详细过程(Maven+Tomcat+热部署)
这篇文章主要介绍了VSCode中开发JavaWeb项目(Maven+Tomcat+热部署),本文分步骤通过图文并茂的形式给大家介绍的非常详细,需要的朋友可以参考下2022-09-09
最新评论