(JavaEE)(多线程案例)线程池 (简单介绍了工厂模式)(含经典面试题ThreadPoolExector构造方法)

news/2024/2/29 4:30:45

线程诞生的意义,是因为进程的创建/销毁,太重了(比较慢),虽然和进程比,线程更快了,但是如果进一步提高线程创建销毁的频率,线程的开销就不能忽视了。

这时候我们就要找一些其他的办法了。

有两种典型的办法可以进一步提高这里的效率:

1: 协程 (轻量级线程,相比于线程,把系统调度的过程给省略了,变成由程序员手工调度)

(当下,一种比较流行的并发编程的手段,但是在Java圈子里,协程还不够流行,GO和Python用的比较多)

2:线程池(Java用的)

接下来我们就来介绍一些线程池 

线程池

优化频繁创建销毁线程的场景 

首先,我们先来了解一下什么是池:

池 

假设一个美女,有很多人追,美女就从这些人里面挑了一个她最喜欢的交往,交往一段时间之后,美女她腻了,她想换一个男朋友,那她接下来就要干两件事

1:想办法和这个男的分手,需要一些技巧,比如挑他毛病,作 之类的

2:再找一个小哥哥,培养感情,然后交往 

但是这样的效率就比较低啦,有没有什么办法来提高一些效率呢?

当然有,只有美女在和前一个交往的时候,和另一个或多个小哥哥,搞暧昧(养🐟),把他们都放到自己的鱼塘里,那和上一个分手,下一个男朋友来的就很快了。 

我们还是只做了两步,只是把第二步提前了。 

“鱼塘”里的这些人,我们通常叫他们 —— “备胎” 

那这个“鱼塘” 就 可以看成 “池” ,来放“备胎”

同样的,线程池,就是在使用第一个线程的时候,提前把 2,3,4,5...(多个)线程创建好(相对于前面的培养感情),那后续我们想使用新的线程的时候,就不必重新创建了,直接拿里的线程用就行了。(此时创建线程的开销就被降低了)

为什么,从池子里取的效率比新创建线程效率高?

这是因为,从池子里取 这个动作,是存粹的 用户态 的操作,而创建新的线程,这个动作,则是需要 用户态 + 内核态 相互配合完成的操作。

内核态 和  用户态 

如果一段程序,是在系统内核中执行的,此时就称为“内核态” ,如果不是,则称为“用户态”

操作系统,是由 内核 + 配套的应用程序 构成的,

内核:系统最核心的部分

创建线程操作,就需要调用系统 api,进入到内核中,按照内核态的方式来完成一系列动作。

内核态的操作要比 纯用户态的操作开销要更大 :至于为什么,我们来举一个例子解释一下:

银行办业务的例子 

首先这个来办理业务的人他不能 进入柜台后面,只能在大厅里,

这个人想来办张银行卡,需要身份证复印件,但是这个人他忘带了,那此时柜台的服务人员就给了他两个选择:

1:把身份证给她,她去帮他复印

2:大厅的角落,有一个自助复印机,他可以去那里自己复印 

那这两个选择中的第二个,自己复印就是纯 用户态操作(这个人可以立即去复印,完事后立即回来办理业务,整个过程非常利落,非常可控

但是如果交给 柜台的服务人员(第一个选择),这个过程就涉及到 内核态 操作了,那此时,你把东西交给他俩,你也不知道柜员消失之后去做了那些事情,也不是的她啥时候回来,整个过程是不可控的。

操作系统内核,是要给所有的进程提供服务的,当你要创建线程的时候,内核虽然会帮你做,但是做的过程中难免也要做一些其他的事情。那在你这边的结果,就不是那么可控。

上述就是内核态 和 用户态的区别 。

Java标准库中的线程池
 

线程池的创建 

我们发现了,线程池这个对象不是我们直接 new 的,而是通过一个专门的方法,返回了一个线程池的对象。 

这种写法就涉及到了 “工厂模式”(校招常考的设计模式)(和上一篇介绍的 单例模式 并列) 

 工厂模式

工厂模式的作用? 

通常我们创建对象 都是使用 new,new 关键字就会触发 类的构造方法,但是构造方法,存在一定的局限性。

“工厂模式” 就是给 构造方法填坑的。 

 那 “工厂模式” 具体是填的什么 坑 呢,我们举一个例子:

 假设 考虑 一个类,来表示平面上的点

然后我们给这个类提供构造方法:

第一个构造方法: 

期待使用笛卡尔坐标系来构造对象。 

 

第二个构造方法:

使用极坐标来构造对象 

但是编译失败了。 

 

 原因:

很多时候,我们希望构造一个对象,可以有多种构造方式 。那多种方式,我们就需要使用多个版本的构造方法来分别实现,但是构造方法要求方法的名字必须是类名,不同的构造方法 只能通过 重载 的方式来区分了,而重载又要求 参数类型 或 个数 不同。

而上面的两个构造方法 很明显没有构成 重载,当然会编译失败。 

这就是 构造方法的局限性 。

“工厂模式”就能解决上述问题 :

使用普通的方法,代替构造方法完成初始化工作,普通的方法就可以使用方法的名字来区分了。也就不受 重载的规则制约了。

工厂模式实践 

在实践中,我们一般单独 搞一个类,然后给这个类搞一些静态方法,由这些静态方法负责构造出对象 

伪代码 

class PointFactory {public static Point makePointByXY(double x, double y) {Point point = new Point();point.setX(x);point.setY(y);return p;}public static Point makePointByRA(double r, double a) {//和上边类似}
} class Demo {public static void main(String[] args) {//使用 Point p = PointFactory.makePointByXY(10,20); }
}

上述介绍之后,我们就知道了为啥 线程池 的 对象我们不直接 new 了

 

这种方法就是 工厂模式 

不同的几种线程池 

第一种: 

此时构造出的线程池对象,有一个基本特点,线程数目是能够动态适应的。

cached: 缓存,用过之后不着急释放,先留着以备下次使用。

也就是说,随着往线程池里添加任务,这个线程池中的线程会根据需要自动被创建出来,创建出来之后也不会着急销毁,会在池子里保留一定的时间,以备随时再使用。

 

除了上边的线程池,我们还有其他的线程池:

第二种 :

这个方法就需要我们指定 创建几个线程,线程个数是固定的 (Fix:固定)

第三种:

只有单个线程的线程池: 

第四种 :

类似于 定时器, 只是 不是只有一个 扫描线程 负责执行任务了,而是有多个线程执行时间到的任务.

 第一种和第二种常用

上述这几个工厂方法生成的线程池,本质上都是对 一个类进行的封装 ——  ThreadPoolExector

ThreadPoolExector 这个类的功能十分丰富,它提供了很多参数,标准库中上述的几个工厂方法,其实就是给这个类填写了不同的参数来构造线程池。 

 ThreadPoolExector 的使用方式

ThreadPoolExector 的核心方法:

1.构造方法

2.注册任务(添加任务)

注册任务(简单):submit 

 

⁜⁜ 构造方法⁜⁜【经典面试题】

构造方法中的参数,很多,且重要, 

我们打开Java文档     Overview (Java Platform SE 8 ) (oracle.com)

打开这个包  juc —— 这个包里放的试和 “并发编程” 相关的内容(Java中,并发编程最主要的体现形式就是多线程)

 

点进这个包然后往下找: 

 

然后我们直接翻到构造方法 :

 

上面的四个构造方法,都差不多,就是参数个数 不一样,第四个 参数最多,能够涵盖上述的三个版本。 

所有我们重点看第四个构造方法: 

 

这一组参数,描述了线程池中,线程的数目: 

 

 

这个线程池里的线程 的数目试可以动态变化的,

变化的范围就是【corePoolSize, maximumPoolSize】

那 “核心线程”  和 “最大线程” 如何理解呢?

如果把一个线程池,理解为一个公司,此时,公司里有两类员工

        1.正式员工

        2.实习生

那正式员工的数目,就是核心线程数,正式员工 + 实习生的数目就是最大线程数

正式员工和实习生的区别:

正式员工,允许摸鱼,不会因为摸鱼被公司开除,有劳动法罩着。

但是实习生,不允许摸鱼,如果这段时间任务多了,此时,就可以多搞几个实习生去干活,如果过段时间任务少了,并且这样的状态还持续了一定时间,那空闲的实习生就可以裁掉了。

这样做,既可以满足效率的要求,又可以,避免过多的系统开销 。

ps: 

 使用线程池,需要设置线程的数目,数目设置多少合适?

 一定不是一个具体的数字!!!因为在接触到实际的项目代码之前,这个数目是无法确定的!!!

一个线程 执行的代码,主要有两类:

1.cpu 密集型:代码里主要的逻辑是在进行 算术运算/逻辑判断。

2.IO 密集型:代码里主要进行的是IO操作。

—— 假设一个线程的所有代码都是 cpu 密集型代码,这个时候,线程池的数量就不应该超过N,就算设置的比N大,此时也无法提高效率,因为cpu吃满了。

—— 假设一个线程的所有代码都是 IO 密集型代码,这个时候不吃cpu,此时设置的线程数,就可以是超过N,(一个核心可以通过调度的方式来并发执行)

上述,我们就知道了,代码不同,线程池的线程数目设置就不同,我们无法知道一个代码,具体多少内容是cpu密集,多少内容是IO密集。所以我们无法确定 数目设置多少合适。

正确做法:使用实验的方式,对程序进行性能测试,测试的过程中尝试修改不同的线程池的线程数目,看那种情况,更符合要求。

这一组参数,描述了允许实习生摸鱼的时间,(实习生不是 一摸鱼就马上被开除)

 

 

这个参数的意思是 阻塞队列 ,用来存放线程池里的任务。

 

可以根据需要,灵活设置这里的队列是啥,比如需要优先级, 就可以设置 PriorityBlockingQueue

如果不需要 优先级,并且任务数目是相对恒定的,可以使用 ArayyBlockingQueue,如果不需要优先级,并且任务数目变动比较大,就可以用 LinkedBlockingQueue

 

这个参数就是 工厂模式的体现 ,此处使用 ThreadFactory 作为 工厂类 由这个类负责创建线程

 

使用工厂类来创建线程,主要是为了在创建线程的过程中,对线程的属性做出一些设置。 

如果手动创建线程,就得手动设置这些属性,就比较麻烦,使用工厂方法封装一下,就更方便。 

 

下面这个参数是最重要的  ,是线程池的拒绝策略

 

一个线程池,能容纳的任务数量,有上限,当持续往线程池里添加任务的时候,一旦达到了上限,还继续添加,会出现什么效果?

拒绝策略就是来解决这个问题的: 不同的拒绝策略有不同的效果。

 

 上面的这四个就是不同的拒绝策略

如果队列满了,再添加就直接抛出异常 

 

新添加的任务,由添加任务的线程负责执行 

 

丢弃最老的任务 

 

丢弃当前新加的任务 

 

 实现一个简单的线程池

这个代码比较简单,就不多说了,代码里都有注释 

import java.awt.*;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;/*** @Author: iiiiiihuang*/
public class ThreadPool {//任务阻塞队列private BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(4);//通过这个方法,把任务添加到队列中public void submit(Runnable runnable) throws InterruptedException {//此处的拒绝策略,相当于第五种策略,阻塞等待(下策)queue.put(runnable);}//构造方法public ThreadPool(int n) {//创建出n个线程,负责执行上诉队列中的任务for (int i = 0; i < n; i++) {Thread t = new Thread(() -> {//让这个线程,从队列中消费任务,并执行try {//取出Runnable runnable = queue.take();//执行runnable.run();} catch (InterruptedException e) {throw new RuntimeException(e);}});t.start();}}
}

 

 

 

关注,点赞,评论,收藏,支持一下╰(*°▽°*)╯╰(*°▽°*)╯


http://www.ppmy.cn/news/1117673.html

相关文章

离散数学之 一阶逻辑等值演算与推理

一阶逻辑等值式与置换规则 基本等值式 这里用到了量词辖域的收缩 未完待续

基于深度强化学习的四旋翼无人机航线跟随

源自&#xff1a;指挥与控制学报 作者&#xff1a;杨志鹏 李波 甘志刚 梁诗阳 “人工智能技术与咨询” 发布 摘 要 针对无人机在空中执行航线跟随任务时无法对未知环境作出合理应对措施等问题, 提出了一种基于深度强化学习的四 旋翼无人机航线跟随方法. 通过无人机受力…

Spring Authorization Server入门 (十八) Vue项目使用PKCE模式对接认证服务

Vue单页面项目使用授权码模式对接流程说明 以下流程摘抄自官网 在本例中为授权代码流程。 授权码流程的步骤如下&#xff1a; 客户端通过重定向到授权端点来发起 OAuth2 请求。 对于公共客户端&#xff0c;此步骤包括生成code_verifier 并计算code_challenge&#xff0c;然后…

jvm垃圾回收机制概述

一、jvm内存的分区 JVM的内存结构包括五大区域&#xff1a;程序计数器、虚拟机栈、本地方法栈、堆区、方法区。 其中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生、随线程而灭&#xff0c;因此这几个区域的内存分配和回收都具备确定性&#xff0c;就不需要过多考虑回收…

Linux查看系统信息

# 查看操作系统的详细信息 uname -a# 查看已安装的Linux发行版信息 cat /etc/os-release# 查看Linux Standard Base (LSB)的信息 lsb_release -a# 查看主机的信息 hostnamectl# 查看文件系统的磁盘空间使用情况 df -h# 查看系统内存的使用情况 free -h# 查看网络接口的信息 ifc…

docker 部署nacos,使用卷数据挂载

使用卷挂载&#xff0c;避免容器删除导致的数据丢失 docker pull nacos/nacos-server docker run --name nacos -d -p 8848:8848 -e MODEstandalone nacos/nacos-server 配置文件数据拷贝 docker cp nacos:/home/nacos/conf/ xx/conf docker stop nacos docker rm nacos 修改…

vue修改node_modules打补丁步骤和注意事项

当我们使用 npm 上的第三方依赖包&#xff0c;如果发现 bug 时&#xff0c;怎么办呢&#xff1f; 想想我们在使用第三方依赖包时如果遇到了bug&#xff0c;通常解决的方式都是绕过这个问题&#xff0c;使用其他方式解决&#xff0c;较为麻烦。或者给作者提个issue&#xff0c;然…

Hive行转列[一行拆分成多行/一列拆分成多列]

场景&#xff1a; hive有张表armmttxn_tmp&#xff0c;其中有一个字段lot_number&#xff0c;该字段以逗号分隔开多个值&#xff0c;每个值又以冒号来分割料号和数量&#xff0c;如&#xff1a;A3220089:-40,A3220090:-40,A3220091:-40,A3220083:-40,A3220087:-40,A3220086:-4…

Java笔记二

学习资源来自哔哩哔哩——遇见狂神说——狂神说Java 目录 数据类型&#xff1a; 字符 字符串 布尔值 强制转换&#xff1a; 变量 常量&#xff1a; 运算符&#xff1a; 数据类型&#xff1a; long定义的必须在数字后面L float定义的要在数字后面加F 如 long num130…

征战MINI学习路线

征战MINI学习路线 征战MINI与ACX720开发板的具体差异 1. 时钟电路 管脚约束一样&#xff0c;仅仅是位号名称不同&#xff0c;ACX720的晶振位号是U2&#xff0c;征战MINI的位号是X1&#xff0c;如下图所示&#xff1a; 2. 拨码开关电路 管脚约束一样&#xff0c;仅仅是位…

druid在springboot中如何整合配置!

在Spring Boot中配置Druid作为数据源非常简单。Druid是一个高性能的数据库连接池&#xff0c;它提供了丰富的监控和统计功能&#xff0c;适用于各种数据库。以下是在Spring Boot中配置Druid数据源的步骤&#xff1a; 1. 添加Druid依赖&#xff1a; 首先&#xff0c;您需要在项…

【UML】类图详解

UML UML ——Unified modeling language UML&#xff08;统一建模语言&#xff09;&#xff0c;是一种用于软件系统分析和设计的语言工具&#xff0c;它用
于帮助软件开发人员进行思考和记录思路的结果 UML图有哪些 用例图静态结构图∶类图、对象图、包图、组件图、部署图动…

chrome浏览器 调试鼠标悬停后出现的元素样式

鼠标悬停后出现的样式&#xff0c;我们遇见的有两种情况&#xff0c;一种是用css设置的hover时的样式&#xff0c;第二种就像el-tooltip组件&#xff0c;鼠标悬停时出现提示文字的样式。 一、css设置的hover时的样式 1、按【F12】键&#xff08;或右键点击【检查】&#xff09…

React(react18)中组件通信04——redux入门

React&#xff08;react18&#xff09;中组件通信04——redux入门 1. 前言1.1 React中组件通信的其他方式1.2 介绍redux1.2.1 参考官网1.2.2 redux原理图1.2.3 redux基础介绍1.2.3.1 action1.2.3.2 store1.2.3.3 reducer 1.3 安装redux 2. redux入门例子3. redux入门例子——优…

23种设计模式汇总详解

设计原则 中文名称英文名称含义解释单一职责原则Single Responsibility Principle(SRP)任何一个软件模块都应该只对某一类行为者负责一个类只干一件事&#xff0c;实现类要单一开闭原则Open-Close Principle(OCP)软件实体&#xff08;类、模块、函数等&#xff09;应该是可以扩…

信息化发展53

数据标准化 1 、数据标准化是实现数据共享的基础。 2 、数据标准化的主要内容包括元数据标准化、数据元标准化、数据模式标准化、数据分类与编码标准化和数据标准化管理。 元数据标准化 1 、元数据是关于数据的数据&#xff08; Data About Data &#xff09;。其实质是用于…

重磅发布!汉威科技燃气安全一站式解决方案全面覆盖燃气安全最新需求

近年来&#xff0c;我国陆续出台相关国家政策、标准等文件&#xff0c;对燃气安全建设提出一系列新要求&#xff0c;尤其是近期国务院安委会印发的《全国城镇燃气安全专项整治工作方案》&#xff0c;将燃气安全建设提升至新的高度。要实现全链条整治并做到标本兼治&#xff0c;…

Go的sync.Mutex互斥锁

我们使用了一个buffered channel作为一个计数信号量&#xff0c;来保证最多只有20个goroutine会同时执行HTTP请求。同理&#xff0c;我们可以用一个容量只有1的channel来保证最多只有一个goroutine在同一时刻访问一个共享变量。一个只能为1和0的信号量叫做二元信号量&#xff0…

maven常用命令

maven的常用命令 mvn -v //查看版本 mvn archetype:create //创建 Maven 项目 mvn compile //编译源代码 mvn test-compile //编译测试代码 mvn test //运行应用程序中的单元测试 mvn site //生成项目相关信息的网站 mvn package //依据项目生成 jar 文件 mvn install /…

BigDecimal正确使用姿势

文章目录 BigDecimal1.0BigDecimal减法1.1 BigDecimal除法1.2 BigDecimal累加1.3 BigDecimal转为double&#xff0c;并累加Bigdecimal转Double并四舍五入保留两位小数1.4 BigDecimalFormat使用1.5 BigDecimal转为StringBigDecimal转为String展示1.6 小数点处理setScale&#xf…
最新文章