首页 > 其他分享 >复现线程池引发的生产环境BUG

复现线程池引发的生产环境BUG

时间:2024-12-26 11:58:33浏览次数:4  
标签:小明 java 任务 线程 复现 executor BUG ThreadPoolExecutor

引言

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

线程池

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

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

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

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

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

@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.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。这种策略适合对任务执行有严格要求的场景,确保所有任务都能被处理。

executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());

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

executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

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

executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());

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

executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());

选择合适的拒绝策略

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

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

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

@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,而是提升了自己作为开发者的能力。

结尾

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

标签:小明,java,任务,线程,复现,executor,BUG,ThreadPoolExecutor
From: https://blog.csdn.net/u012263509/article/details/144352019

相关文章

  • 2. 线程
    这里所说的线程指程序执行过程中的一个线程实体。JVM允许一个应用并发执行多个线程。JVM中的Java线程与原生操作系统线程有直接的映射关系。当线程本地存储、缓冲区分配、同步对象、栈、程序计数器等准备好以后,就会创建一个操作系统原生线程。Java线程结束,原生线程随之被回......
  • Java面试要点99 - Java线程池的关闭过程
    文章目录引言一、线程池的关闭方式1.1shutdown方法1.2shutdownNow方法二、关闭过程中的状态转换2.1线程池状态监控2.2优雅关闭的实现三、任务处理与异常处理3.1关闭时的任务处理3.2关闭过程中的异常处理总结引言线程池的关闭是Java并发编程中的重要环节,......
  • Java面试要点98 - Java中线程池的任务提交过程
    文章目录引言一、任务提交方式1.1execute方法1.2submit方法二、任务执行流程2.1核心流程分析2.2任务状态转换三、任务队列处理3.1队列类型选择3.2队列满时的处理四、异常处理4.1提交时异常处理4.2执行时异常处理总结引言在Java并发编程中,了解线程池的......
  • 19章8节:复现NHANES的美国成人抑郁症患病率研究(下)
     在前一部分中,《复现NHANES的美国成人抑郁症患病率分析(上)》和《用R复现NHANES的美国成人抑郁症患病率分析(中)》分别完成了数据准备与初步探索性分析,并且也计算并呈现了美国成人抑郁症患病率的总体水平及其在人群中的分布差异。为了深入了解性别、年龄等因素对抑郁症患病率的影......
  • Java多线程第三篇-多线程的代码相关案例
    文章目录一.单例模式1.1饿汉模式1.2懒汉模式1.3指令重排序1.4解决方法volatile二.阻塞队列2.1模拟实现阻塞队列(生产者消费者模型)三.定时器(日常开发常见的组建)3.1创建并模拟定时器实现四.线程池4.1线程池的构造方法的使用4.2模拟实现线程池五.锁策略(特点)5.......
  • 打印三角形金字塔 、debug、java的方法、命令行传参、可变参数20241225
    打印三角形金字塔debug20241225packagecom.pangHuHuStudyJava.struct;publicclassPrint_Tran{publicstaticvoidmain(String[]args){for(intj=0;j<5;j++){for(intr=5;r>j;r--){System.out.print(&#......
  • Java 线程池深入剖析:核心概念、源码解析与实战应用
    线程池是现代多线程编程中的重要工具,它能显著提升任务处理效率并优化系统资源。本文将全面解析Java中的线程池机制,帮助开发者深入了解线程池的工作原理、实现方式及其最佳实践。一、基础概念1.什么是线程池?线程池是一种用于管理和复用线程资源的高效工具,能够在程序中......
  • 一个 Bug JDK 居然改了十年?
    问题现象今天偶然看到了一个JDK的Bug,给大家分享一下。假设现在有如下的代码:List<String>list=newArrayList<>();list.add("1");Object[]array=list.toArray();array[0]=1;System.out.println(Arrays.toString(array));上面的代码是可以正常支执行的,如下图所......
  • 最新的强大的文生视频模型Pyramid Flow 论文阅读及复现
    《PYRAMIDALFLOWMATCHINGFOREFFICIENTVIDEOGENERATIVEMODELING》论文地址:2410.05954https://arxiv.org/pdf/2410.05954项目地址:jy0205/Pyramid-Flow:用于高效视频生成建模的金字塔流匹配代码https://github.com/jy0205/Pyramid-Flow论文提出了一种新的视频生成模型,......
  • WordPress File Upload插件 任意文件读取漏洞复现(CVE-2024-9047)(附脚本)
    0x01产品描述:        FileUpload插件是一款功能强大的WordPress站点文件上传插件,它允许用户在WordPress站点中的文章、页面、侧边栏或表单中轻松上传文件到wp-contents目录中的任何位置。该插件使用最新的HTML5技术,确保在现代浏览器和移动设备上都能流畅运行,同时也......