当前位置: 首页 > news >正文

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

对于刚刚从校园出来的菜鸡选手很容易写出自以为没问题的屎山代码,可是当上线后就会立即暴露出问题,这说到底还是基础不够扎实!只会背八股文,却不理解,面试头头是道,一旦落地就啥也不是。此处,抛出几个八股文:为什么要使用线程池?使用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{@Overridepublic void run() {System.out.println("任务开始!");try {System.out.println("体检报告生成中...");Thread.sleep(20000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("将体检报告发送至用户邮箱!");}}@Testpublic 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 对象可以获取任务执行的结果,同时也能捕获到任务抛出的异常。

相关文章:

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

对于刚刚从校园出来的菜鸡选手很容易写出自以为没问题的屎山代码&#xff0c;可是当上线后就会立即暴露出问题&#xff0c;这说到底还是基础不够扎实&#xff01;只会背八股文&#xff0c;却不理解&#xff0c;面试头头是道&#xff0c;一旦落地就啥也不是。此处&#xff0c;抛…...

uniapp 跨页面传值及跨页面方法调用

uniapp 跨页面传值及跨页面方法调用 1、跨页面传值 使用全局方法监听uni.$emit、uni.$on、uni.$off 发布、监听、移除 methods: {addFun(){let data [1]uni.navigateBack({ // 返回上一页delta: 1})uni.$emit(successFun,{data}) // 传值} }监听页 onLoad() {uni.$on(succ…...

无线物理层安全大作业

这个标题很帅 Beamforming Optimization for Physical Layer Security in MISO Wireless NetworksProblem Stateme![在这里插入图片描述](https://img-blog.csdnimg.cn/58ebb0df787c4e23b0c7be4189ebc322.png) Beamforming Optimization for Physical Layer Security in MISO W…...

目标检测标注工具AutoDistill

引言 在快速发展的机器学习领域&#xff0c;有一个方面一直保持不变&#xff1a;繁琐和耗时的数据标注任务。无论是用于图像分类、目标检测还是语义分割&#xff0c;长期以来人工标记的数据集一直是监督学习的基础。 然而&#xff0c;由于一个创新性的工具 AutoDistill&#x…...

关于SPJ表的数据库作业

打字不易&#xff0c;且复制且珍惜 建表 use 库名;create table S( --供应商 SNO char(6) not null, SNAME char(10) not null, STATUS INT, CITY char(10), primary key(SNO));create table P( --零件 PNO char(6) not null, PNAME char(12)not null, COLOR char(4), WEIGHT…...

【Nacos】配置管理、微服务配置拉取、实现配置热更新、多环境配置

&#x1f40c;个人主页&#xff1a; &#x1f40c; 叶落闲庭 &#x1f4a8;我的专栏&#xff1a;&#x1f4a8; c语言 数据结构 javaEE 操作系统 Redis 石可破也&#xff0c;而不可夺坚&#xff1b;丹可磨也&#xff0c;而不可夺赤。 Nacos 一、nacos实现配置管理1.1 统一配置管…...

HTML5学习系列之网页图像

HTML5学习系列之网页图像 前言定义图像定义流定义图标 总结 前言 学习记录 定义图像 标签可以直接把图像插入网页中。 <img src"xx" alt"xx"/>src&#xff1a;显示图像的URLalt&#xff1a;设置图像的替代文本height、width&#xff1a;图像的高度…...

go语言学习之旅之Go语言数据类型

学无止境&#xff0c;今天学习Go 语言数据类型 Go&#xff08;或Golang&#xff09;是一种静态类型语言&#xff0c;这意味着变量的数据类型必须显式声明&#xff0c;并且在运行时不能更改。以下是Go中的一些基本数据类型&#xff1a; 这里仅介绍最常用的类型 数值类型: int: …...

Day49 力扣单调栈 : 739. 每日温度 |496.下一个更大元素 I

Day49 力扣单调栈 : 739. 每日温度 &#xff5c;496.下一个更大元素 I 739. 每日温度第一印象看完题解的思路什么是单调栈?我的总结 实现中的苦难感悟代码 496.下一个更大元素 I第一印象看完题解的思路实现中的困难感悟代码 739. 每日温度 今天正式开始单调栈&#xff0c;这是…...

实用篇-ES-RestClient查询文档

一、快速入门 上面的查询文档都是依赖kibana&#xff0c;在浏览器页面使用DSL语句去查询es&#xff0c;如何用java去查询es里面的文档(数据)呢 我们通过match_all查询来演示基本的API&#xff0c;注意下面演示的是 match_all查询&#xff0c;也叫基础查询 首先保证你已经做好了…...

2023年第九届数维杯国际大学生数学建模挑战赛

2023年第九届数维杯国际大学生数学建模挑战赛正在火热进行&#xff0c;小云学长又在第一时间给大家带来最全最完整的思路代码解析&#xff01;&#xff01;&#xff01; 下面是数维杯B题思路解析&#xff1a; 前面三问主要是绘制趋势图、散点图等这些比较简单的统计学分析方法…...

TensorRT基础知识及应用【学习笔记(十)】

这篇博客为修改过后的转载&#xff0c;因为没有转载链接&#xff0c;所以选了原创 文章目录 一、准备知识1.1 环境配置A. CUDA DriverB. CUDAC. cuDNND. TensorRT 1.2 编程模型 二、构建阶段2.1 创建网络定义2.2 配置参数2.3 生成Engine2.4 保存为模型文件2.5 释放资源 三、运…...

[内存泄漏][PyTorch](create_graph=True)

PyTorch保存计算图导致内存泄漏 1. 内存泄漏定义2. 问题发现背景3. pytorch中关于这个问题的讨论 1. 内存泄漏定义 内存泄漏&#xff08;Memory Leak&#xff09;是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放&#xff0c;造成系统内存的浪费&#xff0c;导致…...

【Git学习二】时光回溯:git reset和git checkout命令详解

&#x1f601; 作者简介&#xff1a;一名大四的学生&#xff0c;致力学习前端开发技术 ⭐️个人主页&#xff1a;夜宵饽饽的主页 ❔ 系列专栏&#xff1a;Git等软件工具技术的使用 &#x1f450;学习格言&#xff1a;成功不是终点&#xff0c;失败也并非末日&#xff0c;最重要…...

多维时序 | MATLAB实现PSO-GRU-Attention粒子群优化门控循环单元融合注意力机制的多变量时间序列预测

多维时序 | MATLAB实现PSO-GRU-Attention粒子群优化门控循环单元融合注意力机制的多变量时间序列预测 目录 多维时序 | MATLAB实现PSO-GRU-Attention粒子群优化门控循环单元融合注意力机制的多变量时间序列预测预测效果基本介绍模型描述程序设计参考资料 预测效果 基本介绍 MAT…...

MySQL缓冲池的优化与性能提升

“不积跬步&#xff0c;无以至千里。” MySQL是许多Web应用的核心数据库&#xff0c;而数据库的性能对于应用的稳定运行至关重要。在MySQL中&#xff0c;缓冲池&#xff08;Buffer Pool&#xff09;是一个关键的组件&#xff0c;它直接影响着数据库的性能和响应速度。今天这篇文…...

一些RLHF的平替汇总

卷友们好&#xff0c;我是rumor。 众所周知&#xff0c;RLHF十分玄学且令人望而却步。我听过有的小道消息说提升很大&#xff0c;也有小道消息说效果不明显&#xff0c;究其根本还是系统链路太长自由度太高&#xff0c;不像SFT一样可以通过数据配比、prompt、有限的超参数来可控…...

7.docker部署前端vue项目,实现反向代理配置

介绍&#xff1a; 构建镜像&#xff1a;通过docker构建以nginx为基础的镜像&#xff0c;将vue项目生成的dist包拷贝至nginx目录下&#xff0c;.conf文件做反向代理配置&#xff1b;部署服务&#xff1a;docker stack启动部署服务&#xff1b; 通过执行两个脚本既可以实现构建…...

字符串函数详解

一.字母大小写转换函数. 1.1.tolower 结合cppreference.com 有以下结论&#xff1a; 1.头文件为#include <ctype.h> 2.使用规则为 #include <stdio.h> #include <ctype.h> int main() {char ch A;printf("%c\n",tolower(ch));//大写转换为小…...

Mybatis学习笔记-映射文件,标签,插件

目录 概述 mybatis做了什么 原生JDBC存在什么问题 MyBatis组成部分 Mybatis工作原理 mybatis和hibernate区别 使用mybatis&#xff08;springboot&#xff09; mybatis核心-sql映射文件 基础标签说明 1.namespace&#xff0c;命名空间 2.select&#xff0c;insert&a…...

C++初阶-list的底层

目录 1.std::list实现的所有代码 2.list的简单介绍 2.1实现list的类 2.2_list_iterator的实现 2.2.1_list_iterator实现的原因和好处 2.2.2_list_iterator实现 2.3_list_node的实现 2.3.1. 避免递归的模板依赖 2.3.2. 内存布局一致性 2.3.3. 类型安全的替代方案 2.3.…...

Python:操作 Excel 折叠

💖亲爱的技术爱好者们,热烈欢迎来到 Kant2048 的博客!我是 Thomas Kant,很开心能在CSDN上与你们相遇~💖 本博客的精华专栏: 【自动化测试】 【测试经验】 【人工智能】 【Python】 Python 操作 Excel 系列 读取单元格数据按行写入设置行高和列宽自动调整行高和列宽水平…...

Python实现prophet 理论及参数优化

文章目录 Prophet理论及模型参数介绍Python代码完整实现prophet 添加外部数据进行模型优化 之前初步学习prophet的时候&#xff0c;写过一篇简单实现&#xff0c;后期随着对该模型的深入研究&#xff0c;本次记录涉及到prophet 的公式以及参数调优&#xff0c;从公式可以更直观…...

Psychopy音频的使用

Psychopy音频的使用 本文主要解决以下问题&#xff1a; 指定音频引擎与设备&#xff1b;播放音频文件 本文所使用的环境&#xff1a; Python3.10 numpy2.2.6 psychopy2025.1.1 psychtoolbox3.0.19.14 一、音频配置 Psychopy文档链接为Sound - for audio playback — Psy…...

HTML前端开发:JavaScript 常用事件详解

作为前端开发的核心&#xff0c;JavaScript 事件是用户与网页交互的基础。以下是常见事件的详细说明和用法示例&#xff1a; 1. onclick - 点击事件 当元素被单击时触发&#xff08;左键点击&#xff09; button.onclick function() {alert("按钮被点击了&#xff01;&…...

用机器学习破解新能源领域的“弃风”难题

音乐发烧友深有体会&#xff0c;玩音乐的本质就是玩电网。火电声音偏暖&#xff0c;水电偏冷&#xff0c;风电偏空旷。至于太阳能发的电&#xff0c;则略显朦胧和单薄。 不知你是否有感觉&#xff0c;近两年家里的音响声音越来越冷&#xff0c;听起来越来越单薄&#xff1f; —…...

【VLNs篇】07:NavRL—在动态环境中学习安全飞行

项目内容论文标题NavRL: 在动态环境中学习安全飞行 (NavRL: Learning Safe Flight in Dynamic Environments)核心问题解决无人机在包含静态和动态障碍物的复杂环境中进行安全、高效自主导航的挑战&#xff0c;克服传统方法和现有强化学习方法的局限性。核心算法基于近端策略优化…...

uniapp手机号一键登录保姆级教程(包含前端和后端)

目录 前置条件创建uniapp项目并关联uniClound云空间开启一键登录模块并开通一键登录服务编写云函数并上传部署获取手机号流程(第一种) 前端直接调用云函数获取手机号&#xff08;第三种&#xff09;后台调用云函数获取手机号 错误码常见问题 前置条件 手机安装有sim卡手机开启…...

搭建DNS域名解析服务器(正向解析资源文件)

正向解析资源文件 1&#xff09;准备工作 服务端及客户端都关闭安全软件 [rootlocalhost ~]# systemctl stop firewalld [rootlocalhost ~]# setenforce 0 2&#xff09;服务端安装软件&#xff1a;bind 1.配置yum源 [rootlocalhost ~]# cat /etc/yum.repos.d/base.repo [Base…...

Web中间件--tomcat学习

Web中间件–tomcat Java虚拟机详解 什么是JAVA虚拟机 Java虚拟机是一个抽象的计算机&#xff0c;它可以执行Java字节码。Java虚拟机是Java平台的一部分&#xff0c;Java平台由Java语言、Java API和Java虚拟机组成。Java虚拟机的主要作用是将Java字节码转换为机器代码&#x…...