异步任务线程池——最优雅的方式创建异步任务

对于刚刚从校园出来的菜鸡选手很容易写出自以为没问题的屎山代码,可是当上线后就会立即暴露出问题,这说到底还是基础不够扎实!只会背八股文,却不理解,面试头头是道,一旦落地就啥也不是。此处,抛出几个八股文:为什么要使用线程池?使用Executors的工厂类创建的线程池会有什么问题?阿里为什么推荐使用标准构造器ThreadPoolExecutor创建线程池?使用线程池提交任务submitexecute 有啥不一样吗?等等,如果以上的问题你回答不出,那么你距离一个中级软件工程师还有很大差距!我建议系统性学习一下,如果只是想知道答案可以访问我的 私人知识库 Java专项中的多线程与线程池篇章 也可阅读我另外一篇博客:Java高并发核心编程(JUC)—线程池详细笔记。本文就以开发中的实际问题作为切入点练习,使得以后初入职场能够快速上手,问题少少!

任务情景

假设你在项目中有个异步操作需要实现,例如用户请求生成体检报告,假设体检报告生成需要5分钟,这时候你可以立即告诉用户体检报告生成中,稍后发送至用户的邮箱。此时,用户就不用在这儿等待了,可以退出系统了。作为菜鸡的我们很可能写出下面的代码:

@Test
public void threadTaskClassical() throws InterruptedException {
    // 模拟100个用户请求
    for (int i = 0; i < 100; i++) {
        new Thread(()->{
            System.out.println("任务开始!");
            try {
                System.out.println("体检报告生成中...");
                Thread.sleep(20000L);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("将体检报告发送至用户邮箱!");
        }).start();
        System.out.println("请求成功!提交报告生成中,稍后发送至您的邮箱!"); 
    }
    Thread.sleep(100000L);
}

上面代码的for循环是用来模拟用户提交生成提交报告请求的,而当用户提交请求将立即得到响应 “请求成功!提交报告生成中,稍后发送至您的邮箱!”。这里为了便于举例,使用 Thread.sleep(20000L); 来模拟5分钟生成体检报告。问题来了:以上代码看起来没啥问题,但是落地后大概率会出现什么问题?会导致虚拟机崩溃!因为,用户的每个请求都将创建一个线程,线程的内存空间是在虚拟机栈内存中的,每开启一个线程将在虚拟机栈内存中开辟一个栈(虚拟机默认一个线程栈内存1MB大小,可以通过 -Xss 参数来设置),而线程中的每个方法的物理内存大小叫做栈帧,存放在栈中。因此,以上代码当用户量过大例如同时有1万人使用,那么会导致栈内存溢出: 每个线程在执行时都有自己的调用栈(stack),其中保存了方法的调用和局部变量等信息。如果过度创建线程,可能导致栈内存溢出,因为每个线程的栈内存是有限的。总的来说,不使用线程池而直接创建大量线程可能导致资源耗尽、栈内存溢出、调度开销增加等问题,从而影响应用程序的稳定性和性能。

使用线程池改进

理解以上理论后,很容易使用线程池来改进,但是依旧会存在问题的哦!我们慢慢来,首先使用更加low一点的线程池来改进,于是写下如下代码:

@Test
public void threadPoolTaskClassical() throws InterruptedException {
    ExecutorService pools = Executors.newFixedThreadPool(10);
    // 模拟100个用户请求
    for (int i = 0; i < 100; i++) {
        pools.execute(()->{
            System.out.println("任务开始!");
            try {
                System.out.println("体检报告生成中...");
                Thread.sleep(20000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("将体检报告发送至用户邮箱!");
        });
        System.out.println("请求成功!提交报告生成中,稍后发送至您的邮箱!");
    }
    Thread.sleep(100000L);
}

这个是使用固定的线程池,即线程池最大只支持同时10个线程同时工作,超过的线程将排队等待其他线程执行完毕。这样的话会导致任务不断进入等待队列,最终等待队列存放不下新来的任务而导致溢出使得虚拟机OOM崩溃!于是,Executors工厂类有各种各样的线程池总结如下:

  • newFixedThreadPool(int nThreads): 创建一个固定大小的线程池,该线程池中的线程数量始终保持不变。如果某个线程因为执行异常而终止,会有新的线程来替代它。
  • newCachedThreadPool(): 创建一个根据需要创建新线程的线程池。线程池的大小没有限制。如果线程在60秒内未被使用,则将其终止并从池中移除。
  • newSingleThreadExecutor(): 创建一个单线程的线程池,该线程池中的线程按顺序执行提交的任务。如果这个唯一的线程在执行任务时抛出异常,将会创建一个新的线程来替代它。
  • newScheduledThreadPool(int corePoolSize): 创建一个固定大小的线程池,支持定时及周期性任务执行。

最后两个方法比较抽象一点,下面给出最后一个的代码实现,这样就能理解了!

ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
// 在延迟2秒后执行任务
scheduledThreadPool.schedule(() -> System.out.println("Delayed task"), 2, TimeUnit.SECONDS);
// 在延迟1秒后,每隔3秒执行一次任务
scheduledThreadPool.scheduleAtFixedRate(() -> System.out.println("Periodic task"), 1, 3, TimeUnit.SECONDS);

以上只是简单介绍一下,详细的内容在我的另外一篇博客中已经讲解的非常清楚了,欢迎阅读:Java高并发核心编程(JUC)—线程池详细笔记

为什么禁止使用Executors快捷创建线程池?

  • FixedThreadPool和SingleThreadPool 这两个工厂方法所创建的线程池,工作队列(任务排队的队列)长度都为Integer.MAX_VALUE,可能会堆积大量的任务,从而导致OOM(即耗尽内存资源)。
  • CachedThreadPool和ScheduledThreadPool 这两个工厂方法所创建的线程池允许创建的线程数量为Integer.MAX_VALUE,可能会导致创建大量的线程,从而导致OOM问题。

所以,大厂的编程规范都不允许使用Executors创建线程池,而是要求使用标准构造器ThreadPoolExecutor创建线程池。

最优雅的方式

大部分企业的开发规范都会禁止使用快捷线程池(具体原因稍后介绍),要求通过标准构造器ThreadPoolExecutor去构造工作线程池。 Executors工厂类中创建线程池的快捷工厂方法实际上是调用ThreadPoolExecutor (定时任务使用ScheduledThreadPoolExecutor )线程池的构造方法完成的。ThreadPoolExecutor构造方法:

public ThreadPoolExecutor(
	int corePoolSize,// 核心线程数,即使线程空闲(Idle), 也不会回收
    int maximumPoolSize,// 线程数的上限
    long keepAliveTime,// 线程最大空闲(Idle)时长
    TimeUnit unit, // 时间单位
    BlockingQueue<Runnable> workQueue, //任务的阻塞排队队列
    ThreadFactory threadFactory, //新线程的产生方式
    RejectedExecutionHandler handler //拒绝策略
)

关键参数介绍:

  • corePoolSize:核心线程数,定义了最小可以同时运行的线程数量,当在线程池接收到的新任务,并且当前工作线程数少于corePoolSize时,即使其他工作线程处于空闲状态,也会创建一个新线程来处理该请求,直到线程数达到corePoolSize。如果当前工作线程数多于corePoolSize数量,但小于maximumPoolSize数量,那么仅当任务排队队列已满时才会创建新线程。通过设置corePoolSizemaximumPoolSize相同,可以创建一个固定大小的线程池。

  • maximumPoolSize:最大线程数,当队列中存放的任务达到队列容量时,当前可以同时运行的数量变为最大线程数,创建线程并立即执行最新的任务,与核心线程数之间的差值又叫救急线程数,救急线程是有空闲时长的 keepAliveTime,当达到最大空闲时长被回收。

  • keepAliveTime:救急线程最大存活时间,当线程池中的线程数量大于corePoolSize的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等到keepAliveTime时间超过销毁。但是如果调用了allowCoreThreadTimeOut(boolean)方法,并且传入了参数true,则keepAliveTime参数所设置的Idle超时策略也将被应用于核心线程。

  • workQueue:阻塞队列,存放被提交但尚未被执行的任务

  • handler:拒绝策略,线程到达最大线程数仍有新任务时会执行拒绝策略

线程池的任务调度流程:

  • 如果当前工作线程数量小于核心线程池数量,执行器总是优先创建一个任务线程,而不是从线程队列中获取一个空闲线程。
  • 如果线程池中总的任务数量大于核心线程池数量,新接收的任务将被加入到阻塞队列中,一直到阻塞队列已满。在核心线程池数量已经用完、阻塞队列没有满的场景下,线程池不会为新任务创建一个新线程。
  • 当完成一个任务的执行时,执行器总是优先从阻塞队列中获取下一个任务,并开始执行,一直到阻塞队列为空,其中所有的缓存任务被取光。
  • 在核心线程池数量已经用完、阻塞队列也已经满了的场景下,如果线程池接收到新的任务,将会为新任务创建一个线程(非核心线程),并且立即开始执行新任务。
  • 在核心线程都用完、阻塞队列已满的情况下,一直会创建新线程去执行新任务,直到池内的线程总数超出maximumPoolSize。如果线程池的线程总数超过maximumPoolSize,线程池就会拒绝接收任务,当新任务过来时,会为新任务执行拒绝策略。
    在这里插入图片描述
    因此,最优雅的方式如下,你可以修改各种参数以满足你的要求:
    class MyTask implements Runnable{
        @Override
        public void run() {
            System.out.println("任务开始!");
            try {
                System.out.println("体检报告生成中...");
                Thread.sleep(20000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("将体检报告发送至用户邮箱!");
        }
    }

    @Test
    public void ThreadPoolExecutorDemo() throws InterruptedException {
        // 创建核心线程为10,最大线程为100,救急线程存活时间为60秒,有界阻塞队列容量为100的线程池
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10,
                100,
                60,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<Runnable>(100));
        for (int i = 0; i < 100; i++) {
            threadPoolExecutor.execute(new MyTask());
//            threadPoolExecutor.submit(new MyTask());
            System.out.println("请求成功!提交报告生成中,稍后发送至您的邮箱!");
        }
        Thread.sleep(100000L);
    }

最后提一嘴,executesubmit 提交任务有啥不同?如果都没有返回值,那么两个方法没啥区别,如果需要获取异步任务的执行结果就用 submit ,并且submit既可以提交Runnable没返回值的任务,也可以提交Callable有返回值的任务。具体区别如下:

  • execute(Runnable command) 方法是 Executor 接口定义的,它用于提交不需要返回结果的任务。execute 方法没有返回值。submit(Runnable task) 方法是 ExecutorService 接口定义的,它也用于提交不需要返回结果的任务,但与 execute 不同,submit 方法返回一个 Future 对象。
  • execute 方法适用于 Runnable 类型的任务。submit 方法不仅适用于 Runnable 类型的任务,还适用于 Callable 类型的任务,即返回结果的任务。
  • execute 方法无法获取任务执行的结果或异常。如果任务抛出异常,调用者无法捕获到。submit 方法返回一个 Future 对象,通过 Future 对象可以获取任务执行的结果,同时也能捕获到任务抛出的异常。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/161892.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

搭建mysql主从错误集合

1 mysqld --verbose --help --log-bin-index/tmp/tmp.Frnt2oibYI mysqld: Cant read dir of /etc/mysql/conf.d/ my.cnf是在/etc/mysql/conf.d/文件夹下&#xff0c;所以挂载的时候不要写/etc/mysql 2 COLLATION utf8_unicode_ci is not valid for CHARACTER SET latin1 配…

Linux系统编程学习 NO.9——git、gdb

前言 本篇文章简单介绍了Linux操作系统中两个实用的开发工具git版本控制器和gdb调试器。 git 什么是git&#xff1f; git是一款开源的分布式版本控制软件。它不仅具有网络功能&#xff0c;还是服务端与客户端一体的软件。它可以高效的处理程序项目中的版本管理。它是Linux内…

Flink(七)【输出算子(Sink)】

前言 今天是我写博客的第 200 篇&#xff0c;恍惚间两年过去了&#xff0c;现在已经是大三的学长了。仍然记得两年前第一次写博客的时候&#xff0c;当时学的应该是 Java 语言&#xff0c;菜的一批&#xff0c;写了就删&#xff0c;怕被人看到丢脸。当时就想着自己一年之后&…

WeTab--颜值与实力并存的浏览器插件

一.前言 现在的浏览器花花绿绿&#xff0c;有大量的广告与信息&#xff0c;令人目不暇接。有没有一款好用的浏览器插件可以解决这个问题呢&#xff1f;我愿称WeTab为版本答案。 WeTab的界面&#xff1a; 干净又整洁。最最关键的是还有智能AI供你服务。 这个WeTabAI就像chatgp…

Sam Altman 被罢免细节曝光,投资 100+ 公司或成「话柄」?

2022 年 11 月&#xff0c;ChatGPT 发布掀起 AI 狂潮。时隔 1 年&#xff0c;2023 年 11 月&#xff0c;ChatGPT 之父、Sam Altman 的一项人事巨变&#xff0c;再次掀起了一场 AI 界的风暴&#xff0c;只是这次并不是技术革命&#xff0c;而是 OpenAI 巨头换帅——Sam Altman 被…

高斯积分-Gaussian Quadrature

https://mathworld.wolfram.com/GaussianQuadrature.html

目标检测—YOLO系列(二 ) 全面解读复现YOLOv1 PyTorch

精读论文 前言 从这篇开始&#xff0c;我们将进入YOLO的学习。YOLO是目前比较流行的目标检测算法&#xff0c;速度快且结构简单&#xff0c;其他的目标检测算法如RCNN系列&#xff0c;以后有时间的话再介绍。 本文主要介绍的是YOLOV1&#xff0c;这是由以Joseph Redmon为首的…

Postman接收列表、数组参数@RequestParam List<String> ids

示例如下: 接口定义如下: GetMapping(value "/queryNewMoviePath")public List<Map<String, Object>> queryNewMoviePath(RequestParam List<String> ids ) {return service.queryNewMoviePath(ids);}postman中测试如下&#xff1a; http://loc…

linux、windows 查看java等进程占用资源情况

linux查看进程占用资源情况&#xff1a; top -o %MEM -b -n 1 | grep java | awk {print "PID: "$1" \t 虚拟内存: "$5" \t 物理内存: "$6" \t 共享内存: "$7" \t CPU使用率: "$9"% \t 内存使用率: "$10"%&…

剑指JUC原理-20.并发编程实践

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是爱吃芝士的土豆倪&#xff0c;24届校招生Java选手&#xff0c;很高兴认识大家&#x1f4d5;系列专栏&#xff1a;Spring源码、JUC源码&#x1f525;如果感觉博主的文章还不错的话&#xff0c;请&#x1f44d;三连支持&…

C/C++ 运用WMI接口查询系统信息

Windows Management Instrumentation&#xff08;WMI&#xff09;是一种用于管理和监视Windows操作系统的框架。它为开发人员、系统管理员和自动化工具提供了一种标准的接口&#xff0c;通过这个接口&#xff0c;可以获取有关计算机系统硬件、操作系统和应用程序的信息&#xf…

Maven依赖管理项目构建工具(保姆级教学---下篇)

对于Maven依赖管理项目构建工具的介绍&#xff0c;我们将其分为上篇和下篇。如果您对文章感兴趣&#xff0c;您可以在此链接中找到上篇详细内容&#xff1a; Maven依赖管理项目构建工具&#xff08;保姆级教学上篇&#xff09;-CSDN博客 一、Maven依赖传递和依赖冲突 1. …

SQL 文本函数

前言 SQL文本函数是SQL语言中非常有用的一类函数&#xff0c;它们用于处理和操作字符串数据。在实际应用中&#xff0c;我们经常需要对数据库中的文本数据进行各种操作&#xff0c;比如提取子串、替换子串、拼接字符串等等。而SQL文本函数可以帮助我们轻松地完成这些任务&#…

Egress Gateway

目录 文章目录 目录本节实战Egress Gateway访问外部服务1.Envoy 转发流量到外部服务2.控制对外部服务的访问3.直接访问外部服务总结 Egress 出口网关1.用 Egress gateway 发起 HTTP 请求2.用 Egress gateway 发起 HTTPS 请求 关于我最后 本节实战 实战名称&#x1f6a9; 实战&…

Scrum框架中的Sprint

上图就是sprint里要做的事。Sprint是scrum框架的核心&#xff0c;是所有的想法、主意转换为价值的地方。所有实现产品目标的必要工作都在sprint里完成&#xff0c;这些工作主要包括Sprint 计划&#xff08;Sprint planning&#xff09;、每日站会&#xff08;Daily Scrum&#…

JavaEE——简单认识HTML

文章目录 一、简单解释 HTML二、认识 HTML 的结构三、了解HTML中的相关标签1.注释标签2.标题标签3.段落标签 p4. 换行标签 br5.格式化标签6.图片标签解释 src解释 alt解释其他有关 img 标签的属性 7.超链接标签 a8.表格标签9.列表标签10.input 标签11. select 下拉菜单以及 div…

大数据研发工程师课前环境搭建

大数据研发工程师课前环境搭建 第一章 VMware Workstation 安装 在Windows的合适的目录来进行安装&#xff0c;如下图 1.1 双击打开 1.2 下一步&#xff0c;接受协议 1.3 选择安装位置 1.4 用户体验设置 1.5 快捷方式 已经准备好安装&#xff0c;点击安装 1.6 安装中 1.7 安装…

现在的各类解释非常混乱,到底什么是智慧城市?

智慧城市&#xff0c;简单来说&#xff0c;就是运用先进的信息和通信技术&#xff0c;让城市管理更加智能、高效&#xff0c;让市民的生活更加便捷、舒适。 在我们日常生活中&#xff0c;智慧城市带来的改变无处不在。 想象一下&#xff0c;当你早上醒来&#xff0c;你的手机已…

栈和队列的初始化,插入,删除,销毁。

目录 题外话 顺序表和链表优缺点以及特点 一.栈的特点 二. 栈的操作 2.1初始化 2.2 栈的销毁 2.3 栈的插入 2.3 输出top 2.4 栈的删除 2.5 输出栈 题外话 顺序表和链表优缺点以及特点 特点&#xff1a;顺序表&#xff0c;逻辑地址物理地址。可以任意访问&#xff0c…