【多线程】线程池

devtools/2024/12/22 18:44:02/

🥰🥰🥰来都来了,不妨点个关注叭!
👉博客主页:欢迎各位大佬!👈

在这里插入图片描述

文章目录

  • 1. 线程池含义和作用
  • 2. 线程池获取线程高效的原因
  • 3. Java 标准库中的线程池
    • 3.1 ExecutorService 接口
    • 3.2 工厂模式
      • 3.2.1 含义
      • 3.2.2 分类
      • 3.2.3 意义
      • 3.2.4 案例
      • 3.2.5 作用
    • 3.3 常见创建线程池的方法
  • 4. ThreadPoolExecutor 类
    • 4.1 构造方法
    • 4.2 四种拒绝策略
  • 5. 手动实现线程池
    • 5.1 完整代码
    • 5.2 实现过程
    • 5.3 如何给线程池设置合适线程数量
    • 5.4 线程池的执行流程

在多线程专栏中,介绍多线程的代码案例有:单例模式、阻塞队列、定时器,本期内容将介绍多线程中的第 4 个案例 —— 线程池,我们一起来看看是怎么回事吧!

1. 线程池含义和作用

线程池是什么呢?看到"池"这个字,我们很容易联想到,字符串常量池,数据库连接池等,有关于"池"这个概念,还是很常见的,池的目的是为了提高效率

线程的创建虽然比进程轻量,但是在频繁创建的情况下,开销也是不可忽略的!因此,希望进一步提高效率,有以下两种:
1)协程,即轻量级线程,目前Java标准库还不支持(C++是标准库层面支持,Java是第三库层面上支持,Go、Python是语法层面上支持,本期内容不作展开)
2)线程池

线程池】是通过预先创建一定数量的线程,并将这些线程放入一个池中,当有任务需要执行时,线程池会从池中取出一个空闲的线程来执行该任务,而不是每来一个任务就创建一个新的线程,任务执行完毕后,线程并不会被销毁,而是返回线程池中等待下一个任务的到来,即提前把线程准备好,创建线程的时候不是直接从系统中申请,而是从池子里拿出来,等到线程不用了,归还给池子

线程池作用:线程池最大的好处即为减少每次创建、销毁线程的损耗

  • 降低资源消耗:通过复用已存在的线程,减少线程创建和销毁的开销,从而节省CPU和内存资源
  • 提高响应速度:当任务来时,可立即从线程池中取出空闲线程来执行任务,无需等待新线程的创建
  • 提高线程的可管理性:线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控

2. 线程池获取线程高效的原因

Q:为什么从线程池获取线程比从系统创建线程更高效呢?
A:从线程池获取线程,纯用户态操作;从系统创建线程,涉及到用户态和内核态之间的切换,纯用户态操作时间是可控的,但涉及到内核态操作,时间就不太可控了!

补充】用户态和内核态相关知识

用户态,内核态是操作系统的基本概念

一个操作系统 = 内核 + 配套的应用程序

其中,内核是操作系统最核心的功能模块集合,如硬件管理,各种驱动,进程管理,内存管理,文件系统等等,而在这个内核需要给上层应用程序提供支持
在这里插入图片描述
比如,在完成打印 hello world 的操作,System.out.println("hello world");,应用程序就要调用系统内核,告诉内核操作,我要进行一个打印字符的操作,内核再通过驱动程序,操作显示器,完成上述打印功能

但是同一时刻可能有很多个应用程序,而内核仅只有一个,内核要给这么多程序提供服务,因此,有时候就会导致服务一定那么及时

举一个生活中的栗子,以便我们更方便理解:
在这里插入图片描述
假如去银行办理业务,如果人很多的时候需要排队,依次办理,这里的大厅就相当于用户态,柜台就相当于内核态,假设该银行仅有 1 个柜台,首先排到的人先办理业务,她想办一张银行卡,只带了身份证却没有带身份证复印件,这个时候,工作人员给她两个方案:一是自己拿着身份证去大厅复印机复印,二是将身份证交给工作人员,由工作人员帮忙在柜台复印
这两种方案在效率上是有差异的:
1)自己去复印,就立即去复印立即回来了,中间不耽误,时间是可控的
2)工作人员去复印,比如工作人员现在肚子不舒服需要去上个厕所,或者是去打个水喝口水,总之工作人员可能还会干点别的事情,最后的结果肯定是可以复印的,但是就没有那么及时了,时间不太可控了~

3. Java 标准库中的线程池

3.1 ExecutorService 接口

Java 标准库中提供 ExecutorService,ExecutorService 在 Java 中是 java.util.concurrent(简称JUC)包下的一个接口,继承 Executor 接口,它代表一个异步执行机制,用于管理和执行异步任务
在这里插入图片描述

ExecutorService 接口定义了一组方法,用于管理线程池,比如:

  • 提交任务:通过 ExecutorService,可以提交任务,比如 Runnable 或 Callable 实例,给线程池来异步执行,而不需要显式地创建和管理线程,通过 submit(Runnable task)submit(Callable<T> task) 提交任务给线程池执行在这里插入图片描述

  • 关闭线程池:通过 shutdown()shutdownNow() 方法来关闭线程池,shutdown() 方法会等待已提交的任务执行完成后再关闭线程池,而 shutdownNow() 方法会尝试停止所有正在执行的任务,并返回等待执行的任务列表

  • 等待任务完成:通过 awaitTermination(long timeout, TimeUnit unit) 方法可以等待线程池中的所有任务完成,或者在指定的超时时间后返回

ExecutorService 的实现类主要有 ThreadPoolExecutor 和 ScheduledThreadPoolExecutor(在本期后续内容,将会继续介绍 ThreadPoolExecutor 类),这里先简单介绍一下这两个类:

  • ThreadPoolExecutor类:一个灵活的线程池实现,允许自定义线程池的核心线程数、最大线程数、空闲线程存活时间、任务队列等参数
  • ScheduledThreadPoolExecutor类ThreadPoolExecutor 的一个子类,它支持在给定延迟后运行命令,或者定期地执行命令

通过 ExecutorService 接口,可以将任务提交给线程池,由线程池自动分配和执行任务,线程池管理线程的创建、复用与销毁,使得多线程任务执行更加的高效与可控!

3.2 工厂模式

3.2.1 含义

工厂模式指的是在创建对象的时候,不再直接使用 new,而是使用一些其它的方法,通常是静态方法,协助我们把对象创建出来

3.2.2 分类

在Java中,工厂模式主要分为三种类型:

  • 简单工厂模式(Simple Factory Pattern),又称为静态工厂方法模式
  • 工厂方法模式(Factory Method Pattern)
  • 抽象工厂模式(Abstract Factory Pattern)

3.2.3 意义

Q:为什么需要工厂模式呢?
A:其实,工厂模式是用来填构造方法的"坑"(实属无奈之举),我们可以知道,如果一个类想要提供不同的构造对象的方式,就需要基于构造方法重载,但是构造方法具有局限性,我们回顾一下构造方法的名称,知道构造方法的名称必须与类名相同,这就会造成一个问题

比如以下这个场景:在平面上,构造一个点,有两种方式
1)通过横纵坐标构造一点,需要传入的参数:x,y
2)根据极坐标构造,需要传入的参数:r,α(其中 r 为点到原点的距离,α 为角度)
在这里插入图片描述
这样的方式可行吗?显然是不行的,因为这两个方法名完全相同,并不能构成重载!

于是引入了工厂模式,解决上述问题,构造一个工厂类,使用静态方法设置属性
在这里插入图片描述

3.2.4 案例

在 Java 中,创建线程池也是如此,通过工厂模式构造线程池,代码如下:

public class ThreadDemo {public static void main(String[] args) {//创建线程池ExecutorService pool =  Executors.newFixedThreadPool(10);//添加任务到线程池中去pool.submit(new Runnable() {@Overridepublic void run() {System.out.println("hello hi");}});}
}

打印结果:
在这里插入图片描述
图解分析:
在这里插入图片描述
通过 ExecutorService newFixedThreadPool 中的源代码可以看到:

在这里插入图片描述

3.2.5 作用

工厂模式,是面向对象设计模式中的一种创建型模式,它提供了一种创建对象的最佳方式,在工厂模式中,可以通过使用工厂类来创建对象,而不是直接在客户端中使用 new 关键字实例化对象,这样在创建对象的时候不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象,使对象的创建和使用分离开来,工厂模式主要解决了接口选择的问题,让类的实例化推迟到子类中进行,降低客户端代码与具体对象的耦合度,使代码更加灵活,维护性较强

3.3 常见创建线程池的方法

Executors 类中静态方法创建线程池的4种常见方式:

  • newFixedThreadPool:创建固定线程数的线程池
  • newCachedThreadPool:创建线程数目动态增长的线程池,即不会设定固定值,按需创建,用完后也不会销毁,留着以后备用
  • newSingleThreadExecutor:创建只包含一个线程的线程池
  • newScheduledThreadPool:类似与定时器Timer,只不过不是扫描线程执行,而是由线程池中的线程执行,设定延迟时间后执行命令,或者定期执行命令

Executors 本质上是 ThreadPoolExecutor 类的封装, ThreadPoolExecutor 类提供了更多的可选参数,可以进一步细化线程池行为的设定, ThreadPoolExecutor 类的参数很多,还是挺抽象的,因此,ThreadPoolExecutor 类使用起来太麻烦了~上述的这些工厂方法都是通过包装 ThreadPoolExecutor 类实现的,使用起来比较简单方便(谁不喜欢方便的东西捏!)

下面我们一起来看看 ThreadPoolExecutorl 类!

4. ThreadPoolExecutor 类

ThreadPoolExecutor 类,是 ExecutorService 接口的一个实现类,是原装的线程池类,上述的所有工厂方法都是对这个类进行进一步的封装

打开 Java 官方文档,查阅 ThreadPoolExecutor 类,可以详细看到 Java 官方文档对这个类的说明

4.1 构造方法

ThreadPoolExecutor 类提供很多灵活的线程池功能,它有如下构造方法:

在这里插入图片描述
可以看到 ThreadPoolExecutor 类的构造方法,参数有很多,最后一个构造方法的参数最多,参数包含前 3 个构造方法的参数,因此,这里以最后一个构造方法为例,深入认识这里面的参数

ThreadPoolExecutor {int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler 
}

参数解释如下:

  • corePoolSize:核心线程数,线程池中始终保持存活的线程数,只有当线程池中的线程数量大于这个值时,非核心线程才会在空闲时间超过 keepAliveTime 后被销毁
  • maximumPoolSize:最大线程数,线程池中允许的最大线程数量,当工作队列满了之后,线程池会创建新线程来处理任务,直到线程数量达到这个最大值
  • keepAliveTime:非核心线程保持存活的时间
  • unit:keepAliveTime 参数的时间单位,(比如毫秒,秒,分钟)
  • workQueue:阻塞队列,用于保存等待执行任务,线程池里要管理很多任务,这些任务也是通过阻塞队列来组织的,程序员可以手动指定给线程池一个队列,此时程序猿就可以很方便可以控制或者获取队列中的信息,其中 submit()方法就是把任务放到该队列中
  • threadFactory:工厂模式,创建线程的辅助类,用于创建新线程的工厂,可以通过线程工厂给自定义线程池中的线程设置名字、属性、优先级等
  • handler:线程池的拒绝策略,如果线程池满了,继续往里面添加任务,如何进行拒绝

上述解释可能比较抽象,我们可以想象一个生活中实际的场景,帮助我们进行理解:

将线程池理解为一个公司,我们知道,公司有正式员工,也有实习生/临时工,线程池中的核心线程可以当作是正式员工,非核心线程可以当作是实习生,即corePoolSize = 正式员工的数量,maximumPoolSize = 正式员工 + 实习生的数量,在公司运转很忙的情况下,公司就会多招几个实习生帮忙干活,就类似于线程池多创建几个非核心线程来帮忙完成任务,等到公司不忙了,闲下来的时候,为节省成本与资源,公司又将实习生辞退,相对应线程池中就是非核心线程被销毁,正式员工是签订劳务合同的,不能随意辞退,即使核心线程处于空闲状态也不会被销毁,是始终存在的,而实习生没有签订劳务合同,只是实习合同,是随时可以辞退的,keepAliveTime 就规定实习生存活的时间,即非核心线程保持存活的时间

下面介绍 4 种拒绝策略,重点来啦!

4.2 四种拒绝策略

在这里插入图片描述
结合生活中的一个实例综合理解,比如你正值课多的时候,忙得焦头烂额,这个时候有个同学找你帮忙一起去个地方完成某个任务

  • ThreadPoolExecutor.AbortPolicy直接抛异常,如果线程池满了,还要继续添加任务,添加操作直接抛出异常(一听到这个消息,直接绷不住了,课也不上了,直接一整个心烦意乱)

  • ThreadPoolExecutor.CallerRunsPolicy添加的线程自己负责执行这个任务(直接怼回去,要去你去,我才不去呢,你自己负责)

  • ThreadPoolExecutor.DiscardOldestPolicy丢弃最老的任务,即丢弃阻塞队列的首元素,不执行了,直接删除(看了一下课表,决定把最早的一节课逃掉去完成这个任务)

  • ThreadPoolExecutor.DiscardPolicy丢弃最新的任务,还是做原来的任务(还是继续上自己的课,这个去完成的任务直接丢弃)

注意

  1. 这几个拒绝策略使用哪个,结合具体场景来确定!!!
  2. 线程池没有依赖阻塞行为,而是通过额外实现了其它逻辑更好地处理这个场景的操作,阻塞有的时候可行,有的时候不可行,线程池中不希望依赖"满了阻塞",其实主要是利用"空了阻塞"(这就好比,你到底去不去完成这个任务,需要给你的同学一个立即的答复,如果阻塞等待的话,你干不了什么事情,这个同学也干不了啥,只能干等着)

5. 手动实现线程池

5.1 完整代码

下述代码实现一个固定线程数量的线程池,代码如下:

import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;class MyThreadPool {//定义一个阻塞队列private BlockingDeque<Runnable> queue = new LinkedBlockingDeque<>();public void submit(Runnable runnable) throws InterruptedException { //像生产任务一样,将任务提交到队列中去queue.put(runnable);}//此处实现一个固定线程数的线程池public MyThreadPool(int n) {for(int i = 0; i < n; i++) {Thread t = new Thread(() -> {try {while(true) {Runnable runnable = queue.take();runnable.run();}} catch (InterruptedException e) {e.printStackTrace();}});//!!!不要忘记启动线程,上述只是创建线程了 需要启动线程~t.start();}}
}
public class ThreadDemo22 {public static void main(String[] args) throws InterruptedException {MyThreadPool pool = new MyThreadPool(10);for(int i = 0; i <= 1000; i++) {//lambda表达式变量捕获规则int number = i;pool.submit(new Runnable() {@Overridepublic void run() {System.out.println("hello" + number);}});}}
}

运行结果如下:

在这里插入图片描述

5.2 实现过程

  1. 手动实现线程池,核心的数据结构是 BlockingQueue,用于存放各个可执行的任务
  2. submit()方法,是提交任务,将任务添加到队列中
  3. 实现一个固定线程数的线程池,MyThreadPool(int n),其中 n 为固定线程数量,在线程池构造方面里面,通过 for 循环,创建 n 个 工作线程,n 个线程并发执行,每个线程的任务是从队列中获取的,并进行执行,即在 while(true) 循环中,既有取队列操作,又有执行的操作;将 while(true) 中,循环条件设置为 true,是不让线程在执行完任务后终止,保持工作线程活跃的状态,如果去掉 true,可以看到,只会执行 n 次,因为在执行完后线程终止了不再执行,线程池数量不够任务数量,就无法处理后续任务
  4. 在主线程中,创建线程池,并通过 for 循环 与 submit() 向阻塞队列提交 1000 个任务,工作线程从队列中获取任务并进行执行
  5. 为什么要将 i 赋值 给 number,而不直接打印 i,这里涉及到 lambda 表达式的变量捕获规则(在介绍 Thread类中有具体介绍),lambda 表达式捕获的变量必须是 final 修饰或者是实际 final,实际 final 是不能被修改的,在这里因为 i 变量被修改了,创建一个新的变量保存 i,即可解决

5.3 如何给线程池设置合适线程数量

在实际开发中如何给线程池设置合适的线程数量呢?

我们要知道,线程不是越多越好,线程的本质上是要在 CPU 上调度的,一个线程池的线程数量设置为多少合适,这需要结合实际情况实际任务决定

  1. CPU 密集型任务:主要做一些计算工作,要在 CPU上运行
  2. IO 密集型任务:主要是等待 IO 操作,比如等待读写硬盘,读写网卡等,不怎么消耗 CPU 资源

极端情况下,如果线程全是使用 CPU 运行,线程数就不应该超过 CPU 核心数(逻辑核心,比如一个电脑是6核12线程,即12个逻辑核心,以12为基准),如果线程全是使用的 IO,则线程数可以设置很多,远远超出 CPU 的核心数

在实际开发中,很少有这么极端的情况,需要具体通过测试的方式来确定,测试方式的大体思路是,运行程序,通过记录时间戳计算一下执行时间,同时监测资源的使用状态,线程数量取一个执行效率可以并且占用资源也还可以的数量

5.4 线程池的执行流程

  1. 任务提交
    一个新的线程任务被提交到线程池时,线程池会首先尝试在线程池中分配一个空闲线程来执行这个任务
  2. 队列处理
    线程池会检查工作队列是否已满,如果工作队列未满,则将新任务放入工作队列中等待,直到有空闲线程取出并执行;如果工作队列已满,且当前线程数已达到最大线程数时,如果再有新任务提交到线程池,则会触发拒绝策略
  3. 线程调度
    根据当前线程池的状态(如空闲线程数、工作队列状态、存活线程数等)来决定如何处理新提交的任务,判断是否创建新线程或是复用空闲线程执行任务
  4. 任务执行
    线程池中的工作线程从任务队列中获取任务并执行,每个线程在执行完任务后继续从任务队列获取下一个任务
  5. 线程回收
    如果线程池中的线程在一定时间内(keepAliveTime)没有新的任务执行,且当前运行的线程数大于核心线程数,非核心线程会被回收,直到线程池中的线程数缩减到核心线程数,核心线程数始终不变
  6. 线程池关闭
    当不再不需要线程时,应显示关闭线程池,释放相关资源(可以看到上述代码打印完成后,并没有结束程序!)

线程池的执行流程是一个动态调整的过程,通过线程池的管理,可以有效地管理和复用线程资源,提高系统的性能和稳定性!

💛💛💛本期内容回顾💛💛💛
在这里插入图片描述

✨✨✨本期内容到此结束啦~


http://www.ppmy.cn/devtools/86572.html

相关文章

甄选范文“论层次式架构在系统中的应用”软考高级论文系统架构设计师论文

论文真题 层次架构作为软件系统设计的一种基本模式,对于实现系统的模块化、可维护性和可扩展性具有至关重要的作用。在软件系统的构建过程中,采用层次架构不仅可以使系统结构更加清晰,还有助于提高开发效率和质量。因此,对层次架构的理解和应用是软件工程师必备的技能之一…

深入探讨 Docker 容器文件系统

引言 随着云计算和微服务架构的兴起&#xff0c;Docker 容器技术迅速成为开发和运维人员的首选工具。Docker 容器不仅提供了一种轻量级的虚拟化方式&#xff0c;还简化了应用程序的部署和管理。在众多的技术细节中&#xff0c;Docker 容器文件系统是一个至关重要的组成部分。本…

Signac包-1.Analyzing PBMC scATAC-seq

–https://stuartlab.org/signac/articles/pbmc_vignette 好的&#xff0c;开始学习scATAC-seq的数据是怎么玩的了&#xff0c;先跑完Signac的教程&#xff0c;边跑边思考怎么跟自己的课题相结合。 留意更多内容&#xff0c;欢迎关注微信公众号&#xff1a;组学之心 数据和R…

科普文:Linux系统安全加固指南

本指南仅关注安全性和隐私性&#xff0c;而不关注性能&#xff0c;可用性或其他内容。 列出的所有命令都将需要root特权。以“$”符号开头的单词表示一个变量&#xff0c;不同终端之间可能会有所不同。 选择正确的Linux发行版 选择一个好的Linux发行版有很多因素。 避免分发…

Python面试题:如何使用GraphQL与Python进行数据查询

要使用 GraphQL 与 Python 进行数据查询&#xff0c;你可以使用一些流行的 GraphQL 客户端库&#xff0c;例如 gql。以下是一个示例&#xff0c;展示了如何使用 gql 库在 Python 中执行 GraphQL 查询。 环境准备 安装 gql 库:pip install gql[requests]示例项目结构 假设你的…

环形链表 II - 力扣(LeetCode)C语言

142. 环形链表 II - 力扣&#xff08;LeetCode&#xff09; (点击前方链接即可查看题目) 给定一个链表的头节点 head &#xff0c;返回链表开始入环的第一个节点。 如果链表无环&#xff0c;则返回 null。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达…

二进制八进制十六进制转十进制,十进制转二进制八进制十六进制,C#与c++实现,学习日志

二进制八进制十六进制转十进制 与相反的C#实现 返回值带有开头&#xff0c;0b,0o,0x&#xff0c;返回值是string类型 static void Main(string[] args){//0b或者0B//0O 或者0//直接写//0x或者0XConvertDecimalToBinary(7).ForEach(x > { Console.Write(x); });Console.Writ…

RabbitMQ高级篇(如何保证消息的可靠性、如何确保业务的幂等性、延迟消息的概念、延迟消息的应用)

文章目录 1. 消息丢失的情况2. 生产者的可靠性2.1 生产者重连2.2 生产者确认2.3 生产者确认机制的代码实现2.4 如何看待和处理生产者的确认信息 3. 消息代理&#xff08;RabbitMQ&#xff09;的可靠性3.1 数据持久化3.2 LazyQueue&#xff08; 3.12 版本后所有队列都是 Lazy Qu…