线程池【开发实践】

文章目录

  • 一、为什么要用线程池
    • 1.1 单线程的问题
    • 1.2 手动创建多线程的问题
    • 1.3 线程池的作用(优点)
    • 1.4 线程池的使用场景
  • 二、线程池的基础知识
    • 2.1 线程池的核心组件
    • 2.2 JUC中的线程池架构
    • 2.3 线程池的配置参数
    • 2.4 线程池常见的拒绝策略(可自定义)
    • 2.5 线程池的生命周期
    • 2.6 线程池的任务调度规则
    • 2.7 线程池执行器的钩子方法
  • 三、使用基础
    • 3.1 常见的工作队列
    • 3.2 Executors提供的线程池
    • 3.3 使用Executors创建线程的问题
  • 四、开发实践
    • 4.1 线程数量配置
    • 4.2 提交任务:以submit(callable)为例
    • 4.3 关闭线程池
  • 参考

一、为什么要用线程池

1.1 单线程的问题

单线程无法利用CPU的多核资源,且存在阻塞问题。使用单个线程来处理一组任务,若当前任务阻塞了线程,该线程将失去CPU的使用权,而不是去执行其他可以执行的任务。

1.2 手动创建多线程的问题

需要开发人员去管理线程的生命周期,麻烦,且可能出错。
还存在并发数不可控的问题,如每次到达一个任务时都新建一个线程来处理,当大量任务到达时,会创建大量的线程,导致线程间的竞争加剧,且可能导致系统资源耗尽。

1.3 线程池的作用(优点)

  • 节省线程创建和销毁的开销:线程池通过复用一组线程,可以节省频繁创建和销毁线程的开销。
  • 控制并发数:线程池可以控制最大并发数,避免线程过多导致系统资源耗尽。
  • 提高响应速度:当任务到达时,可以直接使用线程池中已经创建好的线程,节省了新建线程的时间。
  • 管理线程:线程池负责内部线程的生命周期管理,包括创建、销毁和调度等,无需人工干预。

1.4 线程池的使用场景

  • 处理并发请求:每个请求都要一个线程来处理,使用线程池来复用一组线程,能节省创建和销毁线程的开销。
  • 执行异步任务:避免耗时操作(如邮件发送、文件上传)阻塞主线程,可以使用线程池来异步执行这些任务。
  • 处理IO密集型任务:线程在进行IO操作时会阻塞,使用线程池来调度这些任务,可以在任务阻塞时调度其他任务继续执行,避免CPU空闲。
  • 处理计算密集型任务:使用线程池来执行计算密集型任务,可以充分利用CPU的多核资源,也能控制并发数量避免系统资源耗尽。
  • 后台服务:利用线程池来执行后台服务,与前台线程的执行独立,互不影响。
  • 执行定时任务与周期性任务:可以使用ScheduledThreadPool在后台执行这些任务,与前台线程的执行独立。

二、线程池的基础知识

2.1 线程池的核心组件

  • 线程池管理器:负责线程池的创建、销毁、管理和配置。
  • 工作线程:工作线程是线程池中的实际工作者,负责执行提交给线程池的任务。
  • 任务接口:每个提交到线程池的任务都需要实现一个任务接口。Runnable用于定义无需返回值的任务,而Callable则用于定义可以有返回值的任务,并且可以抛出异常。
  • 工作队列(任务队列):用于暂存等待被执行的任务,有多种实现方式,未必是先进先出。
  • 线程工厂:线程工厂的作用是定义一个规范来创建线程,允许在创建线程时进行定制化设置,例如设置线程的名称、优先级、是否为守护线程(daemon thread)等属性。
  • 拒绝策略:见后。
    (后三个核心组件是线程池的后三个配置参数)

2.2 JUC中的线程池架构

Executor
是线程池管理器的顶层接口,只定义了一个方法void execute(Runnable command),用来提交Runnable任务。

ExecutorService
该接口扩展了Executor,增加了更多管理线程池的方法,如提交Callable任务、提交批量任务、线程池的生命周期管理(shutdown()shutdownNow())、获取线程池状态等。

AbstractExecutorService实现了该接口中的部分方法,为创建自定义的线程池服务实现提供了基础框架。

ThreadPoolExecutor继承自AbstractExecutorService,是JUC中线程池管理器的核心实现类。

Executors是一个静态工厂类,提供了多种线程池管理器。

ScheduledExecutorService
该接口扩展了ExecutorService,增加了定时执行和周期性执行任务的功能。ScheduledThreadPoolExecutor实现该接口,专为计划任务而设计。
在这里插入图片描述

2.3 线程池的配置参数

  • corePoolSize:核心线程数。
  • maximumPoolSize:最大线程数。
  • keepAliveTime:空闲线程存活时间。当线程池中的工作线程数量超过核心线程数时,额外的工作线程在空闲这个时间段后会被终止,直到工作线程数量降到核心线程数。但如果设置了allowCoreThreadTimeOuttrue,那么核心线程也可以被终止。
  • unit:空闲线程存活时间的单位,可以用枚举类TimeUnit设置。
  • workQueue:工作队列。
  • threadFactory:线程工厂。
  • handler:拒绝策略。当阻塞队列已满且工作线程数大于等于最大线程数时,无法接受新的任务,会按照拒绝策略进行处理。

2.4 线程池常见的拒绝策略(可自定义)

  • AbortPolicy:拒绝策略。是默认策略,会将新任务拒绝,并且抛出RejectedExecutionException异常。
  • DiscardPolicy:抛弃策略。会将新任务丢掉,但不会跑出异常。
  • DiscardOldestPolicy:抛弃最老任务策略。不是丢弃新任务,而是先丢弃最先进入队列的任务,然后将新任务入队。
  • CallerRunsPolicy:调用者执行策略。让提交任务的线程去执行新任务,而不是使用线程池中的线程去执行。
    在这里插入图片描述

2.5 线程池的生命周期

  • New:新建状态。当线程池创建后,但尚未开始执行任务时,它处于新建状态。此时,线程池中的线程尚未启动。
  • Running:运行状态。新建的线程池开始接受并处理任务后就会进入运行状态,这是线程池的正常工作状态,可以接收新任务和处理工作队列中的任务。
  • Shutdown:关闭状态。调用shutdown()方法后,线程池会进入关闭状态。此时,线程池不再接受新任务,但不会立刻终止,它会继续处理队列中已有的任务直到所有任务完成。
  • Stop:停止状态。调用shutdownNow()方法后,线程池会进入停止状态,此时会中断正在执行任务的线程,清空任务队列,并返回未开始执行的任务列表。
  • Tidying:整理状态。完成关闭状态或停止状态的工作后,线程池会进入整理状态。此阶段,线程池中的线程数量降为0,即将调用terminated()钩子方法。
  • Terminated:终止状态。在整理状态后,线程池调用terminated()方法并进入终止状态。此时线程池生命周期结束,会释放所有资源。
    在这里插入图片描述

2.6 线程池的任务调度规则

当向线程池提交一个任务时:

  1. 若线程池中的工作线程数量小于核心线程数,执行器总是优先创建一个新的工作线程来执行任务,而不是使用一个空闲的工作线程(目的是快速让线程池中有足够的活跃线程)。若工作线程数量大于等于核心线程数,则根据工作队列的情况进行相应的处理。
  2. 若工作队列未满,则将任务入队。若工作队列已满,则根据工作线程数量与最大线程数的关系进行相应的处理。
  3. 若工作线程数大于等于最大线程数,则执行拒绝策略。否则,会新建一个非核心线程来立即执行新的任务。
    在这里插入图片描述

2.7 线程池执行器的钩子方法

beforeExecute(Thread t, Runnable r): 任务执行之前的钩子方法。
afterExecute(Runnable r, Throwable t): 任务执行之后的钩子方法。
terminated(): 线程池终止时的钩子方法。

三、使用基础

3.1 常见的工作队列

工作队列基于阻塞队列实现,我们可以实现BlockingQueue来自定义阻塞队列,也可以使用JUC中提供的阻塞队列:

  • ArrayBlockingQueue:基于数组的有界阻塞队列。队列大小是固定的,在创建时必须指定。由于是基于数组,所以访问速度快,但可能有扩容受限的问题。
  • LinkedBlockingQueue:基于链表的阻塞队列。如果不指定容量,则默认为Integer.MAX_VALUE。相比ArrayBlockingQueue,插入和删除操作可能稍微慢一点,但由于链表的动态性,它可以更灵活地调整大小。
  • PriorityBlockingQueue:基于最小堆的无界的优先级队列,元素按照自然排序或提供的比较器进行排序。任务按照优先级顺序被处理,而非先进先出。
  • DelayQueue:基于PriorityBlockingQueue的无界阻塞队列,其中元素只有在延迟期满后才能被获取,否则将阻塞等待,常用于实现定时任务和延迟操作。
  • SynchronousQueue:一个特殊的队列,它没有内部容量,每个插入操作必须等待另一个线程的对应移除操作,反之亦然。适用于直接的生产者-消费者传递,非常适合传递性操作。
  • LinkedTransferQueue:一个由链表结构组成的无界阻塞队列,支持公平和非公平模式。它还提供了一个tryTransfer()方法,允许生产者直接将元素转移给等待中的消费者,如果没有消费者等待,则可以选择是否阻塞(生产者可阻塞)。
  • LinkedBlockingDeque:双向链表实现的双向阻塞队列,既可以用作栈,也可以用作队列。它支持有限或无界的队列,并提供了putFirst()putLast()等方法来控制元素的插入位置。

3.2 Executors提供的线程池

在这里插入图片描述

3.3 使用Executors创建线程的问题

  • 定长线程池/单线程话线程池:阻塞队列无界,可能导致JVM出现OOM(Out Of Memory)异常。
  • 定时线程池/可缓存线程池:最大线程数量不设限上,如果任务提交较多,就会造成大量的线程被启动,可能造成OOM异常,也可能导致CPU线程资源耗尽。

四、开发实践

4.1 线程数量配置

IO密集型
核心线程数设置得比CPU核心数稍大,因为当线程在等待I/O时,CPU可以调度其他线程执行。最大线程数可以设置得更高,甚至远大于CPU核心数,具体数值取决于系统的I/O能力及预期的并发水平。一般推荐设置为2 * CPU核心数或更高,但需注意不要设置得过高以免过度消耗系统资源。
计算密集型
核心线程数和最大线程数通常设置为CPU核心数,因为在这种情况下,更多的线程并不能带来性能提升,反而会因为上下文切换带来额外开销。

4.2 提交任务:以submit(callable)为例

	 // 创建线程池
	ExecutorService executor = Executors.newSingleThreadExecutor();

	// 创建Callable任务对象
	Callable<Result> task = () -> {
		// ...
	};

	// 提交任务并获取Future对象
    Future<Result> future = executor.submit(task); // 提交任务并获取Future

    // 获取结果
    try {
    	// get()方法会阻塞,如果任务执行过程中发生了异常,那么此处会抛出该异常
        Result res = future.get();
    } catch (InterruptedException e) {
    	// 处理中断异常...
    } catch (ExecutionException e) {
        // 处理执行异常...
    }
	
	// 关闭ExecutorService
    executor.shutdown();

4.3 关闭线程池

调用shutdown()shutdownNow()后,当前线程不会等待线程池的关闭,若当前线程结束了,可能导致内存泄露和资源浪费。

若线程池中还有活动的线程,就算主线程结束了,JVM也会因为存在活跃的非守护线程而无法退出,导致应用程序无法正常退出。

awaitTermination()方法用来阻塞当前线程,直到线程池中的所有任务完成执行或者超时,或者当前线程被中断。可以用于等待线程池中的线程全部执行完后再退出当前线程,线程池中的所有线程在超时前执行完毕,会返回true并恢复当前线程的执行。

ExecutorService executorService = Executors.newFixedThreadPool(10);

// ... 提交任务到线程池

// 关闭线程池
executorService.shutdown();
try {
    // 等待一定时间,直到超时或者线程池中的线程全部执行结束
    if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
        // 如果超时,则尝试强制关闭
        executorService.shutdownNow();
        // 等待一定时间,直到超时或者线程池中的线程全部执行结束
        if (!executorService.awaitTermination(60, TimeUnit.SECONDS))
            System.err.println("线程池未在规定时间内停止!");
    }
} catch (InterruptedException ie) {
    // 强制终止线程池
    executorService.shutdownNow();
    // 保持当前线程的中断状态
    Thread.currentThread().interrupt();
}

参考

Java线程池(超详细)
Java 多线程:彻底搞懂线程池

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

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

相关文章

看影视学英语(假如第一季第一集)

in the hour也代表一小时吗&#xff1f;等同于in an hour&#xff1f;

科研绘图系列:R语言小提琴图(Violin Plot)

介绍 小提琴图(Violin Plot)是一种结合了箱线图和密度图的图表,它能够展示数据的分布密度和分布形状。以下是对小提琴图的详细解释: 小提琴图能表达: 数据分布:小提琴图通过在箱线图的两侧绘制曲线来展示数据的分布密度,曲线的宽度表示数据点的密度。集中趋势:箱线图部…

渲染农场怎么用更省钱?渲染100邀请码1a12

现在越来越多的设计师开始使用渲染农场&#xff0c;其中收费是个大问题&#xff0c;怎么用渲染农场才能更省钱呢&#xff1f;今天我们就来看下吧。 1、明确渲染方式 要根据不同情况选择合理的渲染方式&#xff0c;比如渲染农场就适合大场景渲染和紧急出图情况&#xff0c;其他…

DDR3 SO-DIMM 内存条硬件总结(一)

最近在使用fpga读写DDR3&#xff0c;板子上的DDR3有两种形式与fpga相连&#xff0c;一种是直接用ddr3内存颗粒&#xff0c;另一种是通过内存条的形式与fpga相连。这里我们正好记录下和ddr3相关的知识&#xff0c;先从DDR3 SO-DIMM 内存条开始。 1.先看内存条的版本 从JEDEC下载…

Python面试宝典第8题:二叉树遍历

题目 给定一棵二叉树的根节点 root &#xff0c;返回它节点值的前序遍历。 示例 1&#xff1a; 输入&#xff1a;root [1,null,2,3] 输出&#xff1a;[1,2,3] 示例 2&#xff1a; 输入&#xff1a;root [] 输出&#xff1a;[] 示例 3&#xff1a; 输入&#xff1a;root […

机器人伦理分析:从扫地机器人到智能伙伴

我发过一个泡泡&#xff1a;机器人和扫地机器人。 意犹未尽&#xff0c;我觉得这是一个值得讨论下去的话题。或者是未来话题 在科技迅猛发展的今天&#xff0c;机器人已经从简单的执行工具演变为能够执行复杂任务的智能实体。特别是在家庭环境中&#xff0c;扫地机器人已经成为…

Python酷库之旅-第三方库Pandas(012)

目录 一、用法精讲 28、pandas.HDFStore.keys函数 28-1、语法 28-2、参数 28-3、功能 28-4、返回值 28-5、说明 28-6、用法 28-6-1、数据准备 28-6-2、代码示例 28-6-3、结果输出 29、pandas.HDFStore.groups函数 29-1、语法 29-2、参数 29-3、功能 29-4、返回…

14-57 剑和诗人31 - LLM/SLM 中的高级 RAG

​​​ 首先确定几个缩写的意思 SLM 小模型 LLM 大模型 检索增强生成 (RAG) 已成为一种增强语言模型能力的强大技术。通过检索和调整外部知识&#xff0c;RAG 可让模型生成更准确、更相关、更全面的文本。 RAG 架构主要有三种类型&#xff1a;简单型、模块化和高级 RAG&…

嵌入式通信协议全解析:SPI、I²C、UART详解(附带面试题)

目录 一、什么是通信 二、 通信的分类 同步通信&#xff08;Synchronous Communication&#xff09; 异步通信&#xff08;Asynchronous Communication&#xff09; 不同协议标准区分图&#xff1a; UART UART的特点&#xff1a; UART的通信过程&#xff1a; UART的配置…

设计模式探索:装饰器模式

1. 装饰器模式定义 装饰器模式&#xff08;Decorator Pattern&#xff09; 装饰器模式是一种结构型设计模式&#xff0c;允许向一个对象动态添加行为。在不改变类的接口的情况下&#xff0c;装饰器模式在原始类上增加额外的职责&#xff0c;并且支持多个装饰器嵌套使用。 装…

使用树莓派进行python开发,控制电机的参考资料

网站连接&#xff1a;https://www.cnblogs.com/kevenduan?page1 1、简洁的过程步骤&#xff0c; 2、有代码示例&#xff0c; 3、有注意事项&#xff0c;

数据结构--二叉树相关习题5(判断二叉树是否是完全二叉树 )

1.判断二叉树是否是完全二叉树 辨别&#xff1a; 不能使用递归或者算节点个数和高度来判断。 满二叉树可以用高度和节点来判断&#xff0c;因为是完整的。 但是完全二叉树前面是满的&#xff0c;但是最后一层是从左到右连续这种 如果仍然用这种方法的话&#xff0c;如下图…

零信任网络安全

随着数字化转型的发生&#xff0c;网络边界也在不断被重新定义&#xff0c;因此&#xff0c;组织必须使用新的安全方法重新定义其防御策略。 零信任是一种基于“永不信任&#xff0c;永远验证”原则的安全方法&#xff0c;它强调无论在公司内部或外部&#xff0c;任何用户、设…

电脑文件夹怎么设置密码?让你的文件更安全!

在日常使用电脑的过程中&#xff0c;我们常常会有一些需要保护的个人文件或资料。为了防止这些文件被他人未经授权访问&#xff0c;对重要文件夹设置密码是一种有效的保护措施&#xff0c;可是电脑文件夹怎么设置密码呢&#xff1f;本文将介绍2种简单有效的方法帮助您为电脑文件…

数据结构基础--------【二叉树题型】

1、前提(待补充) 1.**DFS&#xff08;Depth First Search&#xff09;&#x1f617;*递归法得到最终的数组&#xff08;深度优先算法&#xff09; 其过程简要来说是对每一个可能的分支路径深入到不能再深入为止&#xff0c;如果遇到死路就往回退&#xff0c;回退过程中如果遇…

简易限流实现

需求描述 写一个1秒两个的限流工具类&#xff0c;2r/s 使用semaphore 代码实现-类似令牌桶算法 public class LimitHelper {private int maxLimit;private Semaphore semaphore;private int timeoutSeconds;public LimitHelper(int maxLimit, int timeoutSeconds) {this.max…

OpenAI与Thrive Global推出Thrive AI Health:AI驱动的健康教练应用

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

MMGPL: 多模态医学数据分析与图提示学习| 文献速递-基于深度学习的多模态数据分析与生存分析

Title 题目 MMGPL: Multimodal Medical Data Analysis with Graph Prompt Learning MMGPL: 多模态医学数据分析与图提示学习 01 文献速递介绍 神经学障碍&#xff0c;包括自闭症谱系障碍&#xff08;ASD&#xff09;&#xff08;Lord等&#xff0c;2018年&#xff09;和阿…

虚拟内存【Linux】

虚拟内存 为什么需要虚拟内存Linux虚拟内存的结构32位系统下的虚拟地址空间64位系统下的虚拟地址空间页表多级页表TLB 流程虚拟内存的作用 为什么需要虚拟内存 为了在进行多进程编码进行内存访问的时候保持内存的隔离性&#xff0c;数据安全性&#xff0c;所以出现了虚拟内存。…

PPI(每英寸像素数)、DPI(每英寸点数)和Pixel(像素)的区别和联系?

一、定义 PPI、DPI和Pixel是图像处理、打印和显示领域中常用的三个概念&#xff0c;它们之间既有区别又有联系。以下是对这三个概念进行分别讲解&#xff1a; 1. PPI&#xff08;Pixels Per Inch&#xff09;&#xff0d;即每英寸像素数&#xff0c;是图像分辨率的一种表示方…