没有合理使用线程池,引发的生产环境BUG

引言

随着多线程和并发处理需求的增加,线程池成为了提升系统性能的重要工具。Java 提供了强大的 ThreadPoolExecutor 类,能够高效地管理线程池,减少线程创建和销毁的开销。然而,当线程池达到其最大容量时,如何优雅地处理被拒绝的任务就成为了一个关键问题。本文将深入探讨 Java 线程池的拒绝策略,帮助开发者理解并实现高效稳定的并发应用。

线程池

线程池是一种用于管理和复用线程的机制。它通过维护一个固定数量的线程来处理多个任务,减少了频繁创建和销毁线程带来的性能损耗。Java 的 ThreadPoolExecutor 类是实现线程池的核心,提供了丰富的配置选项以满足不同的应用需求。

线程池的工作原理 __ThreadPoolExecutor** 的工作原理可以简单描述为:

  • 核心线程数:线程池中始终保持的线程数量。
  • 最大线程数:线程池中允许的最大线程数量。
  • 任务队列:存放待执行任务的队列,当核心线程数已满且还有任务到来时,任务将被放入队列中。
  • 饱和策略:当线程池和任务队列均已满时,如何处理新提交的任务。。

小明是一名刚入职不久的 Java 开发工程师,在一家快速发展的初创公司工作。公司专注于在线教育平台,用户量不断攀升。随着用户的增加,网站的性能瓶颈愈发明显。为了提升系统的响应速度,小明被指派负责实现一个新的并发任务处理模块,任务是用线程池来处理用户的请求。

在项目开始时,小明充满激情,迅速地搭建了一个简单的线程池,使用 ThreadPoolExecutor 来管理并发任务。他的初步设计如下:

java 复制代码
@Configuration
public class ThreadPoolConfig {

    @Bean
    public ThreadPoolTaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(100);
        executor.initialize();
        return executor;
    }
}

小明以为这样就能轻松应对用户请求的高峰期,便开始在后台提交任务。他的信心来源于对线程池的基本了解和对项目的热爱。

问题出现

几周后,系统上线了新的任务处理模块,最初的几天一切运行良好。然而,随着用户量的不断增加,问题随之而来。在一次用户登录高峰期,系统突然出现了"请求超时"的错误,用户反馈无法正常使用网站。

小明接到报告后,立刻开始排查问题。他打开日志,发现大量的 RejectedExecutionException 错误,提示任务被拒绝执行。这一消息让他心里一沉,意识到自己的设计出现了问题。

日志中的错误信息如下:

java 复制代码
java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@17ebe8b3[Not completed, task = java.util.concurrent.Executors$RunnableAdapter@9c0c0bb[Wrapped task = com.neo.service.TaskService$$Lambda$810/0x000000080060a840@2bd99784]] rejected from java.util.concurrent.ThreadPoolExecutor@63bf4c3c[Running, pool size = 10, active threads = 10, queued tasks = 100, completed tasks = 0]
	at java.base/java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2055) ~[na:na]
	at java.base/java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:825) ~[na:na]
	at java.base/java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1355) ~[na:na]

小明脑中回忆起之前对线程池的理解:线程池有一个核心线程数和一个最大线程数,任务队列的容量有限。随着任务的不断提交,线程池已经达到最大负载,导致新任务被拒绝。

深思熟虑

小明感到非常沮丧,他意识到自己的设计并没有考虑到高并发场景下的任务处理能力。他开始反思自己的选择,心想:"我该如何解决这个问题?是增加线程池的容量,还是更改任务的处理逻辑?"经过深思熟虑,他决定先在本地环境中进行实验,以找出更好的解决方案。

他创建了一个小型的模拟程序,使用不同的线程池配置和拒绝策略,测试其对任务提交和处理的影响。小明发现,增加线程池的最大容量和任务队列的大小能够有效地处理更多的并发请求,但在极端情况下,仍然可能遇到拒绝执行的情况。

寻求帮助

在一次团队会议上,小明把自己的发现和想法分享给了同事们。资深的同事李工听了小明的描述后,给了他一些建议:

  1. 增加线程池容量:适当调整核心线程数和最大线程数,提升处理能力。
  2. 使用不同的拒绝策略 :考虑使用 CallerRunsPolicyDiscardOldestPolicy,根据业务需求选择合适的策略。
  3. 优化任务逻辑:如果任务处理时间过长,考虑将长任务拆分为短任务,减少单个任务的执行时间。

小明深受启发,决定结合这些建议,重新设计任务处理模块。

重构代码

常见的拒绝策略

拒绝策略是在任务提交被拒绝时的处理方式。

AbortPolicy(默认策略):默认的拒绝策略是 AbortPolicy,当任务被拒绝时,它会抛出 RejectedExecutionException。这种策略适合对任务执行有严格要求的场景,确保所有任务都能被处理。

java 复制代码
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());

CallerRunsPolicy: 策略会将被拒绝的任务交由调用者线程执行。这种策略有效地减缓了任务的提交速度,适合需要动态调整负载的场景。

java 复制代码
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

DiscardPolicy: 策略中,被拒绝的任务将被丢弃,不会抛出任何异常。这种策略适合对丢弃任务没有影响的场景,如低优先级的批处理任务。

java 复制代码
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());

DiscardOldestPolicy: 策略会丢弃任务队列中最旧的任务,并尝试提交当前任务。这有助于保持任务队列的活跃性,适合对新任务更为关注的场景。

java 复制代码
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());

选择合适的拒绝策略

选择合适的拒绝策略取决于应用的需求和任务的优先级。

  • AbortPolicy:当任务执行有严格要求时使用,确保不会丢失任何任务。
  • CallerRunsPolicy:需要动态调整负载时使用,可以有效减缓任务提交速率。
  • DiscardPolicy:适合低优先级任务,可以接受任务丢失的场景。
  • DiscardOldestPolicy:适合需要保持任务活跃的场景,优先丢弃最旧的任务。

经过几天的努力,小明对代码进行了重构。他首先增加了线程池的最大容量和任务队列的大小,并选择了 CallerRunsPolicy 拒绝策略,这样在任务被拒绝时,调用者线程会执行该任务,从而减缓提交速度。

java 复制代码
@Configuration
public class ThreadPoolConfig {

    @Bean
    public ThreadPoolTaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(100);
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

小明还将任务的执行逻辑进行了优化,减少了每个任务的处理时间。经过这些改进,他在本地环境进行了充分的测试,系统稳定性大大提升。

验证一下效果:我们可以看到系统是没有出现错误的,线程也一直被消费。

上线与监控

经过团队的评审和测试后,小明的改进方案得到了批准,新的任务处理模块上线了。上线初期,系统运行平稳,没有出现拒绝执行的情况。为了确保系统在高负载下依然稳定,小明还设置了监控,实时观察线程池的状态。

我们也可以通过JPS和Jstack命令来查看堆栈信息的情况。

也可以使用 Java 提供的 JMX 和 APM 等工具,监控线程池的活跃度、任务数量和拒绝任务的数量。通过这些监控,他能够及时发现潜在问题并进行调整。

我们打开另一个命令行窗口,输入jconsole启动JavaSE自带的一个JMX客户端程序:

总结与反思

通过这次经历,小明学到了很多。他认识到,合理使用线程池是保证系统性能的关键,同时,设计时必须考虑到高并发场景下的各种情况。此外,团队合作和同事的建议对他而言都是极为重要的,及时沟通与反馈是解决问题的有效途径。

小明的故事是一个关于成长和学习的过程。从最初的无知到最终的成熟,他不仅仅是修复了一个 BUG,而是提升了自己作为开发者的能力。

结尾

在技术的道路上,我们都可能遇到挑战和困难。正如小明所经历的,通过不断的学习、反思和改进,我们能够克服这些困难,成为更优秀的开发者。希望每位读者都能在自己的旅程中,像小明一样,迎接挑战,收获成长。

相关推荐
玄尺_00716 小时前
bug:uniapp运行到微信开发者工具 白屏 页面空白
uni-app·bug
賢8431 天前
bug之浮点数精度求和计算
bug
喜欢猪猪1 天前
高并发场景秒杀抢购超卖Bug实战重现
bug
moxiaoran57531 天前
IDEA 未启用lombok插件的Bug
java·bug·intellij-idea
樱木...1 天前
Bug Mark:MySQL 8.0、8.4 部分版本 DDL 操作会导致数据丢失
数据库·mysql·bug
听风吟丶2 天前
Java Bug 修复:实战经验与技巧
java·开发语言·bug
Irises`2 天前
前端页面导出word
前端·word·bug
微网天下2 天前
Mac Goland dlv 升级
ide·golang·bug·mac·brew
多方通行82 天前
关于Ubuntu的server版本登录无法输入password问题
linux·开发语言·ubuntu·编辑器·bug
WellTung_6662 天前
LightRAG测试BUG
linux·python·bug