异步任务线程池——最优雅的方式创建异步任务
对于刚刚从校园出来的菜鸡选手很容易写出自以为没问题的屎山代码,可是当上线后就会立即暴露出问题,这说到底还是基础不够扎实!只会背八股文,却不理解,面试头头是道,一旦落地就啥也不是。此处,抛出几个八股文:为什么要使用线程池?使用
Executors的工厂类创建的线程池会有什么问题?阿里为什么推荐使用标准构造器ThreadPoolExecutor创建线程池?使用线程池提交任务submit和execute有啥不一样吗?等等,如果以上的问题你回答不出,那么你距离一个中级软件工程师还有很大差距!我建议系统性学习一下,如果只是想知道答案可以访问我的 私人知识库 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数量,那么仅当任务排队队列已满时才会创建新线程。通过设置corePoolSize和maximumPoolSize相同,可以创建一个固定大小的线程池。 -
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);}
最后提一嘴,execute 和 submit 提交任务有啥不同?如果都没有返回值,那么两个方法没啥区别,如果需要获取异步任务的执行结果就用 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 对象可以获取任务执行的结果,同时也能捕获到任务抛出的异常。
相关文章:
异步任务线程池——最优雅的方式创建异步任务
对于刚刚从校园出来的菜鸡选手很容易写出自以为没问题的屎山代码,可是当上线后就会立即暴露出问题,这说到底还是基础不够扎实!只会背八股文,却不理解,面试头头是道,一旦落地就啥也不是。此处,抛…...
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 Beamforming Optimization for Physical Layer Security in MISO W…...
目标检测标注工具AutoDistill
引言 在快速发展的机器学习领域,有一个方面一直保持不变:繁琐和耗时的数据标注任务。无论是用于图像分类、目标检测还是语义分割,长期以来人工标记的数据集一直是监督学习的基础。 然而,由于一个创新性的工具 AutoDistill&#x…...
关于SPJ表的数据库作业
打字不易,且复制且珍惜 建表 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】配置管理、微服务配置拉取、实现配置热更新、多环境配置
🐌个人主页: 🐌 叶落闲庭 💨我的专栏:💨 c语言 数据结构 javaEE 操作系统 Redis 石可破也,而不可夺坚;丹可磨也,而不可夺赤。 Nacos 一、nacos实现配置管理1.1 统一配置管…...
HTML5学习系列之网页图像
HTML5学习系列之网页图像 前言定义图像定义流定义图标 总结 前言 学习记录 定义图像 标签可以直接把图像插入网页中。 <img src"xx" alt"xx"/>src:显示图像的URLalt:设置图像的替代文本height、width:图像的高度…...
go语言学习之旅之Go语言数据类型
学无止境,今天学习Go 语言数据类型 Go(或Golang)是一种静态类型语言,这意味着变量的数据类型必须显式声明,并且在运行时不能更改。以下是Go中的一些基本数据类型: 这里仅介绍最常用的类型 数值类型: int: …...
Day49 力扣单调栈 : 739. 每日温度 |496.下一个更大元素 I
Day49 力扣单调栈 : 739. 每日温度 |496.下一个更大元素 I 739. 每日温度第一印象看完题解的思路什么是单调栈?我的总结 实现中的苦难感悟代码 496.下一个更大元素 I第一印象看完题解的思路实现中的困难感悟代码 739. 每日温度 今天正式开始单调栈,这是…...
实用篇-ES-RestClient查询文档
一、快速入门 上面的查询文档都是依赖kibana,在浏览器页面使用DSL语句去查询es,如何用java去查询es里面的文档(数据)呢 我们通过match_all查询来演示基本的API,注意下面演示的是 match_all查询,也叫基础查询 首先保证你已经做好了…...
2023年第九届数维杯国际大学生数学建模挑战赛
2023年第九届数维杯国际大学生数学建模挑战赛正在火热进行,小云学长又在第一时间给大家带来最全最完整的思路代码解析!!! 下面是数维杯B题思路解析: 前面三问主要是绘制趋势图、散点图等这些比较简单的统计学分析方法…...
TensorRT基础知识及应用【学习笔记(十)】
这篇博客为修改过后的转载,因为没有转载链接,所以选了原创 文章目录 一、准备知识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. 内存泄漏定义 内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致…...
【Git学习二】时光回溯:git reset和git checkout命令详解
😁 作者简介:一名大四的学生,致力学习前端开发技术 ⭐️个人主页:夜宵饽饽的主页 ❔ 系列专栏:Git等软件工具技术的使用 👐学习格言:成功不是终点,失败也并非末日,最重要…...
多维时序 | MATLAB实现PSO-GRU-Attention粒子群优化门控循环单元融合注意力机制的多变量时间序列预测
多维时序 | MATLAB实现PSO-GRU-Attention粒子群优化门控循环单元融合注意力机制的多变量时间序列预测 目录 多维时序 | MATLAB实现PSO-GRU-Attention粒子群优化门控循环单元融合注意力机制的多变量时间序列预测预测效果基本介绍模型描述程序设计参考资料 预测效果 基本介绍 MAT…...
MySQL缓冲池的优化与性能提升
“不积跬步,无以至千里。” MySQL是许多Web应用的核心数据库,而数据库的性能对于应用的稳定运行至关重要。在MySQL中,缓冲池(Buffer Pool)是一个关键的组件,它直接影响着数据库的性能和响应速度。今天这篇文…...
一些RLHF的平替汇总
卷友们好,我是rumor。 众所周知,RLHF十分玄学且令人望而却步。我听过有的小道消息说提升很大,也有小道消息说效果不明显,究其根本还是系统链路太长自由度太高,不像SFT一样可以通过数据配比、prompt、有限的超参数来可控…...
7.docker部署前端vue项目,实现反向代理配置
介绍: 构建镜像:通过docker构建以nginx为基础的镜像,将vue项目生成的dist包拷贝至nginx目录下,.conf文件做反向代理配置;部署服务:docker stack启动部署服务; 通过执行两个脚本既可以实现构建…...
字符串函数详解
一.字母大小写转换函数. 1.1.tolower 结合cppreference.com 有以下结论: 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(springboot) mybatis核心-sql映射文件 基础标签说明 1.namespace,命名空间 2.select,insert&a…...
基于算法竞赛的c++编程(28)结构体的进阶应用
结构体的嵌套与复杂数据组织 在C中,结构体可以嵌套使用,形成更复杂的数据结构。例如,可以通过嵌套结构体描述多层级数据关系: struct Address {string city;string street;int zipCode; };struct Employee {string name;int id;…...
【kafka】Golang实现分布式Masscan任务调度系统
要求: 输出两个程序,一个命令行程序(命令行参数用flag)和一个服务端程序。 命令行程序支持通过命令行参数配置下发IP或IP段、端口、扫描带宽,然后将消息推送到kafka里面。 服务端程序: 从kafka消费者接收…...
智慧医疗能源事业线深度画像分析(上)
引言 医疗行业作为现代社会的关键基础设施,其能源消耗与环境影响正日益受到关注。随着全球"双碳"目标的推进和可持续发展理念的深入,智慧医疗能源事业线应运而生,致力于通过创新技术与管理方案,重构医疗领域的能源使用模式。这一事业线融合了能源管理、可持续发…...
【Redis技术进阶之路】「原理分析系列开篇」分析客户端和服务端网络诵信交互实现(服务端执行命令请求的过程 - 初始化服务器)
服务端执行命令请求的过程 【专栏简介】【技术大纲】【专栏目标】【目标人群】1. Redis爱好者与社区成员2. 后端开发和系统架构师3. 计算机专业的本科生及研究生 初始化服务器1. 初始化服务器状态结构初始化RedisServer变量 2. 加载相关系统配置和用户配置参数定制化配置参数案…...
Rust 异步编程
Rust 异步编程 引言 Rust 是一种系统编程语言,以其高性能、安全性以及零成本抽象而著称。在多核处理器成为主流的今天,异步编程成为了一种提高应用性能、优化资源利用的有效手段。本文将深入探讨 Rust 异步编程的核心概念、常用库以及最佳实践。 异步编程基础 什么是异步…...
Cilium动手实验室: 精通之旅---13.Cilium LoadBalancer IPAM and L2 Service Announcement
Cilium动手实验室: 精通之旅---13.Cilium LoadBalancer IPAM and L2 Service Announcement 1. LAB环境2. L2公告策略2.1 部署Death Star2.2 访问服务2.3 部署L2公告策略2.4 服务宣告 3. 可视化 ARP 流量3.1 部署新服务3.2 准备可视化3.3 再次请求 4. 自动IPAM4.1 IPAM Pool4.2 …...
如何配置一个sql server使得其它用户可以通过excel odbc获取数据
要让其他用户通过 Excel 使用 ODBC 连接到 SQL Server 获取数据,你需要完成以下配置步骤: ✅ 一、在 SQL Server 端配置(服务器设置) 1. 启用 TCP/IP 协议 打开 “SQL Server 配置管理器”。导航到:SQL Server 网络配…...
加密通信 + 行为分析:运营商行业安全防御体系重构
在数字经济蓬勃发展的时代,运营商作为信息通信网络的核心枢纽,承载着海量用户数据与关键业务传输,其安全防御体系的可靠性直接关乎国家安全、社会稳定与企业发展。随着网络攻击手段的不断升级,传统安全防护体系逐渐暴露出局限性&a…...
Java 与 MySQL 性能优化:MySQL 慢 SQL 诊断与分析方法详解
文章目录 一、开启慢查询日志,定位耗时SQL1.1 查看慢查询日志是否开启1.2 临时开启慢查询日志1.3 永久开启慢查询日志1.4 分析慢查询日志 二、使用EXPLAIN分析SQL执行计划2.1 EXPLAIN的基本使用2.2 EXPLAIN分析案例2.3 根据EXPLAIN结果优化SQL 三、使用SHOW PROFILE…...
基于单片机的宠物屋智能系统设计与实现(论文+源码)
本设计基于单片机的宠物屋智能系统核心是实现对宠物生活环境及状态的智能管理。系统以单片机为中枢,连接红外测温传感器,可实时精准捕捉宠物体温变化,以便及时发现健康异常;水位检测传感器时刻监测饮用水余量,防止宠物…...
