线程池:优化多线程管理的利器

引言

同步和异步想必各位都有了解,同步简单来说就是一件事做完再去做下一件;异步则是不用等一件事做完,就可以去做另一件事,当一件事完成后可以收到对应的通知;异步一般应用于一些耗时较长的操作,比如大型文件的上传、第三方接口的请求等,可以大大提升系统效率,优化用户体验;但是对于执行这些异步任务的线程管理相对复杂,什么时候异步线程执行结束?如何控制线程的创建数目?线程繁忙时最新的任务该如何处理(舍弃 or 暂存)?这些问题就大大增加了异步编程的复杂性,但是线程池可以很方便的解决这些问题。

在多线程编程中,线程的创建和销毁是一项开销较大的任务。为了更有效地利用系统资源、提高程序的性能,线程池应运而生。线程池是一种管理和复用线程的机制,通过预先创建一定数量的线程,它们可以被重复利用来处理多个任务,避免了线程频繁创建和销毁的开销,提升了系统的性能和响应速度。

线程池有以下优点:

  • 提高系统性能:线程池在高并发的场景中能够有效地提高系统的性能。通过限制并发线程的数量,避免系统因过多线程而产生过多的上下文切换,提升整体系统的处理能力。

  • 控制资源并发度:在资源有限的环境下,线程池可以用来控制并发度,确保系统资源不被过度占用。例如,在进行文件下载时,可以通过线程池限制同时下载的文件数量,避免过多的网络连接占用带宽。

  • 提高响应速度:线程池能够在任务到达时立即执行,而不需要等待新线程的创建,从而提高了系统对外响应的速度。这在需要快速响应用户请求的网络服务中尤为重要。

  • 避免线程泄漏:通过线程池,线程的生命周期由线程池进行管理,可以避免线程泄漏问题。当任务执行完毕后,线程池会将线程放回线程池,而不是销毁它,从而减少了资源浪费。

  • 任务排队和管理:线程池提供了对任务的排队和管理机制。可以按照优先级、先进先出等规则对任务进行排队,确保高优先级的任务先得到执行。

  • 避免系统崩溃:在某些情况下,如果系统同时创建大量线程,可能导致系统资源耗尽,甚至引发系统崩溃。线程池通过控制线程的数量,可以防止系统因为线程过多而崩溃的情况发生。

总的来说线程池主要帮我们解决线程的管理和任务的存取功能,实现更方便的管理线程和协调任务的执行;

线程池的实现

java提供的juc包中已经提供了很方便的方法区创建线程池的类:ThreadPoolExecutor,主要介绍一下该方法的参数:

public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)

ThreadPoolExecutor 是 Java 中用于管理线程池的类,用于执行异步任务。在创建 ThreadPoolExecutor 实例时,可以传递一系列参数来配置线程池的行为

  • corePoolSize: 表示线程池的基本大小,即线程池中保持活动状态的最小线程数。(核心线程数,系统能同时工作的线程数;理解为一个公司的核心成员,不会被开除)

  • maximumPoolSize: 表示线程池的最大大小,即线程池中允许的最大线程数。当工作队列满了,且活动线程数小于最大线程数时,会创建新的线程来处理任务。(最大线程数,理解为一个公司的最多所有成员数目,核心成员外包成员(会被开除),当项目太多的时候(工作队列满),就开始招聘外包成员,但招的外包成员加上核心成员不会超过maximumPoolSize)

  • keepAliveTime: 表示线程空闲时的存活时间。当线程池中的线程数量超过 corePoolSize,且某个线程(非核心线程)空闲时间超过 keepAliveTime,该线程将被终止,直到线程池中的线程数不超过 corePoolSize。(当项目完成后(工作队列空),且等待keepAliveTime这么长时间后,开除外包员工(销毁非核心线程))

  • unit:keepAliveTime 配合使用,表示 keepAliveTime 的时间单位,通常是秒、毫秒等。

  • workQueue: 用于保存等待执行的任务的阻塞队列(工作队列)。可以选择不同的队列实现,如 LinkedBlockingQueueArrayBlockingQueue 等,用于控制任务的排队策略。(工作队列需要设置一个合适的长度,太长了也会占用系统资源)

  • threadFactory: 用于创建新线程的工厂。提供了一种自定义线程创建的机制,可以用来设置线程的名称、优先级等。

  • handler: 表示当线程池中的工作线程达到最大线程数并且阻塞队列已满时的饱和策略。可以选择使用预定义的策略,查看源码可以看到提供有四中策略:
    在这里插入图片描述
    其中默认策略为AbortPolicy:
    image-20240125222517330

    AbortPolicy (默认策略): 默认饱和策略。当任务无法提交给线程池执行时,会抛出 RejectedExecutionException 异常,通知调用者任务无法被接受。

    CallerRunsPolicy: 将任务返回给调用线程来执行(一般是主线程)。这种策略不会抛出异常,而是尝试在调用线程中执行该任务。

    DiscardPolicy: 会默默地放弃无法处理的任务,不提供任何通知或记录。

    DiscardOldestPolicy: 在队列满的情况下,尝试将最早进入队列的任务移除,为新任务腾出空间,然后将新任务添加到队列中,如果失败则按该策略不断重试,不会抛出异常。

    当然也可以自定义策略实现。
    需要根据实际情况合理设置核心线程数、最大线程数和阻塞队列,可以在不同的场景下平衡线程池的性能和资源消耗。


下面画图理解一下:

假设核心线程有2个(corePoolSize),线程池最多能有4个线程(maximumPoolSize),任务队列最多能容纳4个任务(workQueue)。

(2个核心员工,最多再招2两个外包)

初始状态

image-20240125204125140

来了两个任务

当线程池有空闲线程时,有了最新任务不进任务队列,直接分配给空闲线程处理;

image-20240125204920882

任务1和任务2未处理完(无空闲线程),此时又来了四个任务

此时并不会创建非核心线程处理最新任务,而是先把最新任务加入任务队列存储,此时任务队列已满

image-20240125212301991

任务1和任务2未处理完(无空闲线程),并且任务队列已有四个任务已满,此时又来了两个任务

此时线程池就会新增两个非核心线程(线程池最多容纳四个线程),用来处理新来的两个任务,但不是优先处理队列中的任务;

(若此时只来了一个任务,则只创建一个非核心线程处理该任务)

image-20240125212354612

若线程池已满,且无空闲线程,并且任务队列已满,此时若又来了一个任务

这时就会触发饱和策略handler进行处理,比如舍弃该任务,或者主线程介入处理等,默认情况时拒绝处理,会抛出RejectedExecutionException异常;

image-20240125212457519

当线程3和线程4空闲后(此时所有任务已处理完),当空闲时间达到最大空闲时间时,线程3和线程4被释放,线程池只剩下线程1和线程2两个核心线程;


这就是线程池的处理大致流程,一定要记住那几个线程池核心参数,结合图中内容理解。

在实际开发中难点主要是如何确定线程池参数,一定要涉及到多方面的考虑,包括应用的性质、系统资源、任务的性质等。这里提供一些判断的思路;

corePoolSize: 根据应用的负载情况和任务的性质设置。如果任务是计算密集型,并且负载比较高,可以增加 corePoolSize。对于 I/O 密集型任务,可以适度减小 corePoolSize

maximumPoolSize: 根据系统资源和任务性质进行设置。如果任务是 I/O 密集型,并且可能发生阻塞,可以适度增加 maximumPoolSize。但是,不宜过度设置,以免占用过多的系统资源。

keepAliveTime 和 unit: 适度设置,避免线程过早终止或过长存活。如果任务的执行时间相对较短,可以考虑设置较短的 keepAliveTime,一般设置为秒级或者分钟级。

workQueue: 选择适合应用场景的队列类型。对于有界队列(如 ArrayBlockingQueue),可以帮助控制线程数,防止无限制的线程增长,但也可能导致任务被拒绝。无界队列(如 LinkedBlockingQueue)可以避免任务被拒绝,但可能导致线程数无限增长。

threadFactory: 根据需要进行设置,可以用于为线程指定有意义的名称、设置优先级等。

handler: 根据应用的容错需求选择合适的饱和策略。例如,CallerRunsPolicy 可以将任务退回给调用线程,避免任务丢失,但可能导致调用线程也过载。

补充一下IO密集型和计算密集型:

一般情况下,任务可以分为IO密集型和计算密集型(CPU密集型);

  • 对于IO密集型任务,主要由于涉及到大量的读写操作,如文件操作、网络通信、数据库访问等操作,在这种任务类型中,大部分时间线程都在等待 I/O 操作完成,而不是在执行计算操作,即对CPU的利用率不高。由于线程大部分时间都在等待 I/O 操作,可以使用较大的核心线程数和最大线程数,以确保有足够的线程可以处理等待的 I/O 操作,提高任务响应性,但是还是要根据设备的I/O性能来设定。
  • 对于计算密集型任务,其中涉及大量的计算操作,例如数学运算、图形处理等。在这种任务类型中,线程大部分时间都在执行计算操作,而不是等待 I/O 操作。所以可以使用较小的核心线程数和最大线程数,避免占用过多的系统资源和减少线程间的冲突,因为增加线程数可能导致竞争和上下文切换。一般将核心线程数设置为 CPU 的核数加一。这个"加一”可以理解为预留一个额外的线程,或者说一个备用线程,用来处理其他任务。这样做可以充分利用每个CPU 核心,减少线程间的频繁切换,降低开销;在这种情况下,对 maximumPoolSize 的设定没有严格的规则,一般可以设为核心线程数的两倍或三倍。

补充思路:反向压力可以动态控制线程数,简单来说系统压力小时可以多增加几个线程,压力大时减少几个线程,或者可以系统压力大时使用异步,压力小时使用同步,这一块属于大数据内容,后续学习了再补充;

代码实现

下面用一个代码案例模拟一下,以便更直观的理解和应用;

还是用上面的案例:假设核心线程有2个(corePoolSize),线程池最多能有4个线程(maximumPoolSize),任务队列最多能容纳4个任务(workQueue),非核心线程释放时间为100s

先自定义一个线程池的Bean

@Configuration
public class ThreadPoolExecutorConfig {@Beanpublic ThreadPoolExecutor threadPoolExecutor() {ThreadFactory threadFactory = new ThreadFactory() { // 自定义线程工厂的private int count = 1;@Overridepublic Thread newThread( Runnable r) {Thread thread = new Thread(r);thread.setName("线程" + count); // 设置线程名称count++;return thread;}};ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 4, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<>(4), threadFactory);return threadPoolExecutor;}
}

此时没有设置饱和策略,使用的是默认AbortPolicy策略,即抛出异常;

写一个测试Controller

@RestController
@RequestMapping("/queue")
@Slf4j
public class QueueController {@Resourceprivate ThreadPoolExecutor threadPoolExecutor;// 新增任务@GetMapping("/add")public void add(String name) {CompletableFuture.runAsync(() -> {log.info("任务执行中:" + name + ",执行线程:" + Thread.currentThread().getName());try {Thread.sleep(600000); // sleep模拟执行一个耗时的任务} catch (InterruptedException e) {e.printStackTrace();}}, threadPoolExecutor);}// 获取当前线程池状态@GetMapping("/get")public String get() {Map<String, Object> map = new HashMap<>();int size = threadPoolExecutor.getQueue().size();map.put("queue size", size);long taskCount = threadPoolExecutor.getTaskCount();map.put("task count", taskCount);long completedTaskCount = threadPoolExecutor.getCompletedTaskCount();map.put("complete task", completedTaskCount);int activeCount = threadPoolExecutor.getActiveCount();map.put("active thread", activeCount);return JSONUtil.toJsonStr(map);}
}

调用add接口,先新增两个任务(1、2),则两个核心线程(线程1和线程2)被占用:

在这里插入图片描述
在这里插入图片描述

控制台输出:
image-20240125224715891
再增加四个任务(3、4、5、6),此时任务队列已满:
在这里插入图片描述
断点调试get接口可以看到任务队列中等待的任务:
image-20240125225006584

再增加两个任务(7、8),则非核心线程会创建(线程3和线程4)处理这两个任务,此时共4个线程,已达到最大线程数:

在这里插入图片描述
image-20240125225201924
此时如果再增加任务(9),则会触发默认AbortPolicy饱和策略,抛出异常:

在这里插入图片描述

直到任务队列有空位时才可以继续增加任务。

线程1、2、3、4执行完各自的任务后依次执行队列中的任务:

在这里插入图片描述
在这里插入图片描述

最后非核心线程执行完成后100s后空闲自动释放:
在这里插入图片描述

这就是大致的线程池操作了,线程池核心参数和整个任务分配和线程管理的流程是关键。

总结

线程池是优化并发执行的关键工具,通过合理配置参数、选择适当的任务队列和饱和策略,可以充分发挥其优势。在实际应用中,根据任务类型、负载和性能需求进行调整,动态监控线程池的状态,将使得线程池更好地适应不同的应用场景,提高系统的并发性能。

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

分布式锁的应用场景及实现

文章目录 分布式锁的应用场景及实现1. 应用场景2. 分布式锁原理3. 分布式锁的实现3.1 基于数据库 分布式锁的应用场景及实现 1. 应用场景 电商网站在进行秒杀、特价等大促活动时&#xff0c;面临访问量激增和高并发的挑战。由于活动商品通常是有限库存的&#xff0c;为了避免…

LabVIEW读取excel日期

LabVIEW读取excel日期 | Excel数据表格中有日期列和时间列&#xff0c;如下表所示&#xff1a; 通过LabVIEW直接读取Excel表格数据&#xff0c;读出的日期列和时间列数据与原始表格不一致&#xff0c;直接读出来的数据如下表所示&#xff1a; 日期、时间列数据异常 问题产生原因…

Sora新视角:从介绍到商业价值,全面解读优势

关于作者 还是大剑师兰特&#xff1a;曾是美国某知名大学计算机专业研究生&#xff0c;现为航空航海领域高级前端工程师&#xff1b;CSDN知名博主&#xff0c;GIS领域优质创作者&#xff0c;深耕openlayers、leaflet、mapbox、cesium&#xff0c;canvas&#xff0c;webgl&…

使用Sora部署实时音视频通信应用实战项目

一、项目概述 本项目将构建一个在线教学平台&#xff0c;实现教师与学生之间的实时音视频通信。平台将提供教师上传课件、发起授课邀请&#xff0c;学生加入课堂、实时互动等功能。通过使用Sora&#xff0c;我们将确保音视频通信的稳定、流畅和低延迟。 目录 一、项目概述 二…

目录IO 2月19日学习笔记

1. lseek off_t lseek(int fd, off_t offset, int whence); 功能: 重新设定文件描述符的偏移量 参数: fd:文件描述符 offset:偏移量 whence: SEEK_SET 文件开头 SEE…

linux基础命令和示例

redis在go语言中的使用 以下说明以读者有redis基础的前提下进行 未学习redis的可以到b站1小时浅学redis了解大概&#xff0c;学会如何使用 【GeekHour】一小时Redis教程_哔哩哔哩_bilibili 以下开发环境以windows为测试环境&#xff0c;旨在练习redis在go语言中的使用 red…

电池可热插拔拆卸对三防加固平板有什么意义|亿道三防onerugged

今天我要和大家聊聊三防加固平板电脑中一个非常重要的功能——电池的可热插拔拆卸。是的&#xff0c;亿道三防onerugged系列产品具备这一亮点功能&#xff0c;给用户带来了极大的便利和灵活性。 首先&#xff0c;让我们来看看电池可热插拔拆卸的优势之一——双电池设计。亿道三…

AGI|一篇小白都能看懂的RAG入门介绍!

目录 一、前言 二、LLM主要存在的问题 三、RAG 是什么&#xff1f; 四、RAG中的搜索器 &#xff08;一&#xff09;主要的检索技术 &#xff08;二&#xff09;知识库索引技术 五、RAG目前遇到的问题和展望 一、前言 随着近几年AIGC的发展&#xff0c;不仅是大模型自身在…

如何查看 CPU 占用高的进程

1、使用 top 命令&#xff0c;查看 cpu 占用超过 100% 2、查看哪个进程占用 cpu 最高&#xff08;该案例使用阿里的 arthas 来查看&#xff09; 2.1 下载&#xff1a;curl -O https://arthas.aliyun.com/arthas-boot.jar 2.2 启动命令&#xff1a;java -jar arthas-boot.jar …

【C语言的小角落】逻辑与逻辑或混合计算

关注小庄 顿顿解馋(≧◡≦) 引言&#xff1a;本篇博客小庄带领小伙伴们解决一个比较角落有时头疼的问题—关于逻辑与和逻辑或结合运算的问题&#xff0c;请放心食用~ 我们先放代码说话 int main() {int x 1;int y 3;int z 4;if(x1 || y && z){;} printf("y …

前端新手Vue3+Vite+Ts+Pinia+Sass项目指北系列文章 —— 第十一章 基础界面开发 (组件封装和使用)

前言 Vue 是前端开发中非常常见的一种框架&#xff0c;它的易用性和灵活性使得它成为了很多开发者的首选。而在 Vue2 版本中&#xff0c;组件的开发也变得非常简单&#xff0c;但随着 Vue3 版本的发布&#xff0c;组件开发有了更多的特性和优化&#xff0c;为我们的业务开发带…

第十四章[面向对象]:14.1:类和实例

一,认识面向对象编程 1,什么是面向对象编程? 面向对象编程——Object Oriented Programming,简称OOP,是一种程序设计思想。OOP把对象作为程序的基本单元,一个对象包含了数据和操作数据的函数。 2,面向对象最重要的两个概念就是类(Class)和实例(Instance) 类是抽象的…