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

【多线程】线程池

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

在这里插入图片描述

文章目录

  • 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. 线程池关闭
    当不再不需要线程时,应显示关闭线程池,释放相关资源(可以看到上述代码打印完成后,并没有结束程序!)

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

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

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

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 详解Qt 之 QColor、QColorSpace与QColorTransform
  • JSONP跨域
  • IDEA优化配置,提高启动和运行速度
  • 什么牌子的洗地机好用?推荐多款质量好洗地机的品牌
  • 说下Linux特点,与windows的区别
  • 【C++标准库】模拟实现string类
  • 【Python】pandas:排序、重复值、缺省值处理、合并、分组
  • 【numpy】浮点数比较大小
  • Oat++ 后端实现跨域
  • Linux非常强大的一个命令find
  • 通过知识管理提升财务卓越性
  • QT自适应布局中尺寸控制相关的resize、resizeEvent分析
  • 【前端手写代码】手写instanceof方法
  • Python 中的 NotImplemented 和 NotImplementedError
  • HTTP协议详解(一)
  • 【笔记】你不知道的JS读书笔记——Promise
  • 2019年如何成为全栈工程师?
  • Docker容器管理
  • gitlab-ci配置详解(一)
  • iOS仿今日头条、壁纸应用、筛选分类、三方微博、颜色填充等源码
  • JavaScript 基本功--面试宝典
  • markdown编辑器简评
  • MYSQL如何对数据进行自动化升级--以如果某数据表存在并且某字段不存在时则执行更新操作为例...
  • RxJS: 简单入门
  • Vue 重置组件到初始状态
  • 创建一个Struts2项目maven 方式
  • 电商搜索引擎的架构设计和性能优化
  • 对象管理器(defineProperty)学习笔记
  • 前端 CSS : 5# 纯 CSS 实现24小时超市
  • 如何在GitHub上创建个人博客
  • 入职第二天:使用koa搭建node server是种怎样的体验
  • ​DB-Engines 11月数据库排名:PostgreSQL坐稳同期涨幅榜冠军宝座
  • ​LeetCode解法汇总2182. 构造限制重复的字符串
  • ​业务双活的数据切换思路设计(下)
  • # 达梦数据库知识点
  • ###STL(标准模板库)
  • #pragam once 和 #ifndef 预编译头
  • (ISPRS,2021)具有遥感知识图谱的鲁棒深度对齐网络用于零样本和广义零样本遥感图像场景分类
  • (附源码)springboot太原学院贫困生申请管理系统 毕业设计 101517
  • (附源码)计算机毕业设计ssm基于Internet快递柜管理系统
  • (十三)Flask之特殊装饰器详解
  • (四)进入MySQL 【事务】
  • (转)全文检索技术学习(三)——Lucene支持中文分词
  • (自用)仿写程序
  • (自用)网络编程
  • ***利用Ms05002溢出找“肉鸡
  • .aanva
  • .mkp勒索病毒解密方法|勒索病毒解决|勒索病毒恢复|数据库修复
  • .net framework 4.8 开发windows系统服务
  • .NET 中选择合适的文件打开模式(CreateNew, Create, Open, OpenOrCreate, Truncate, Append)
  • .NET/C# 使用反射调用含 ref 或 out 参数的方法
  • .Net程序猿乐Android发展---(10)框架布局FrameLayout
  • .net解析传过来的xml_DOM4J解析XML文件
  • /bin/rm: 参数列表过长"的解决办法
  • /run/containerd/containerd.sock connect: connection refused