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

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

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

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

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.mzph.cn/news/83483.shtml

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

相关文章

Ansible之Playbook的任务控制

一&#xff09;Ansible 任务控制基本介绍 这⾥主要来介绍PlayBook中的任务控制。 任务控制类似于编程语⾔中的if … 、for … 等逻辑控制语句。 这⾥我们给出⼀个实际场景应⽤案例去说明在PlayBook中&#xff0c;任务控制如何应⽤。 在下⾯的PlayBook中&#xff0c;我们创建了…

pnpm入门教程

一、概述 1、更小 使用 npm 时&#xff0c;依赖每次被不同的项目使用&#xff0c;都会重复安装一次。 而在使用 pnpm 时&#xff0c;依赖会被存储在内容可寻址的存储中。 2、更快 依赖解析。 仓库中没有的依赖都被识别并获取到仓库。目录结构计算。 node_modules 目录结构是…

什么是GPT磁盘?介绍GPT(GUID 分区表)磁盘及其优势!

GPT概述 GPT磁盘是什么意思&#xff1f;GPT是全局唯一标识符分区表&#xff08;GUID Partition Table&#xff09;的简称&#xff0c;它是硬盘分区表结构的一个标准模式。在我们深入了解GPT磁盘的特性之前须知&#xff0c;MBR磁盘的分区信息直接保存在主引导记录&#xff0…

【探索C语言中VS调试技巧】:提高效率和准确性

文章目录 前言1. 什么是bug&#xff1f;2. 调试是什么&#xff1f;有多重要&#xff1f;2.1 调试是什么&#xff1f;2.2 调试的基本步骤2.3 Debug和Release的介绍 3. Windows环境调试介绍3.1 调试环境的准备3.2 学会快捷键3.3 调试的时候查看程序当前信息3.3.1 查看临时变量的值…

【C++】动态内存管理 ③ ( C++ 对象的动态创建和释放 | new 运算符 为类对象 分配内存 | delete 运算符 释放对象内存 )

文章目录 一、C 对象的动态创建和释放1、C 语言 对象的动态创建和释放 的方式2、C 语言 对象的动态创建和释放 的方式 二、代码示例 - 对象的动态创建和释放 一、C 对象的动态创建和释放 使用 C 语言中的 malloc 函数 可以为 类对象 分配内存 ; 使用 free 函数可以释放上述分配…

Android 富文本SpannableString

一、认识SpannableString 为什么要使用富文本 在Android开发中&#xff0c;有很多UI会画出一些特别炫酷的界面出来&#xff0c;比如一个字符串里有特殊的字会有其他颜色并加粗、变大变小、插入小图片、给某几个文字添加边框&#xff0c;如果我们使用笨办法用几个TextView或者Im…

单片机第三季-第三课:STM32开发板原理图、配置、浮点运算单元

目录 1&#xff0c;开发板原理图 2&#xff0c;浮点运算单元&#xff08;FPU&#xff09; 1&#xff0c;开发板原理图 课程视频比较早&#xff0c;介绍了三款开发板。观看视频时用的开发板说和51单片机共板的STM32核心板&#xff0c;将51单片机从底座拆下来后&#xff0c;安…

avi怎么转换成视频?

avi怎么转换成视频&#xff1f;在我们日常使用的视频格式中&#xff0c;AVI是一种常见且经常被使用的音频视频交叉格式之一。它的优点之一是占用的存储空间相对较小&#xff0c;但也明显存在着画质损失的缺点。虽然AVI格式的视频在某种程度上也很常见&#xff0c;但与最常见的M…

zabbix的原理与安装

一、Zabbix介绍 1、zabbix 是什么&#xff1f; zabbix是一个开源的IT基础监控软件&#xff0c;能实时监控网络服务&#xff0c;服务器和网络设备的状态&#xff0c;如网络使用&#xff0c;CPU负载、磁盘空间等&#xff0c;主要是包括数据的收集、报警和通知的可视化界面zabbi…

VHOST-SCSI代码分析(1)VHOST SCSI设备模拟

VHOST SCSI设备的模拟是由QEMU和HOST共同实现的&#xff0c;QEMU模拟VHOST SCSI设备配置空间等&#xff0c;而对于虚拟机通知HOST和HOST通知虚拟机机制由HOST内核实现。 在QEMU中VHOST SCSI设备继承关系如下&#xff1a; 其它设备以及对应class_init函数和realize具现化实现与V…

LLM微调(一)| 单GPU使用QLoRA微调Llama 2.0实战

最近LLaMA 2在LLaMA1 的基础上做了很多优化&#xff0c;比如上下文从2048扩展到4096&#xff0c;使用了Grouped-Query Attention&#xff08;GQA&#xff09;共享多头注意力的key 和value矩阵&#xff0c;具体可以参考&#xff1a; 关于LLaMA 2 的细节&#xff0c;可以参考如下…

zotero通过DOI快速导入文献

之前我经常采用两种方式导入文献&#xff1a; &#xff08;1&#xff09;下载PDF&#xff0c;然后拖入zotero 这种方法比较费时间&#xff0c;有些文献无法下载pdf &#xff08;2&#xff09;通过google scholar检索文献&#xff0c;然后点击引用——EndNote&#xff0c;chorme…

Kotlin中函数的基本用法以及函数类型

函数的基本用法 1、函数的基本格式 2、函数的缺省值 可以为函数设置指定的初始值&#xff0c;而不必要传入值 private fun fix(name: String,age: Int 2){println(name age) }fun main(args: Array<String>) {fix("张三") }输出结果为&#xff1a;张三2 …

WebGL层次模型——多节点模型

目录 多节点模型 MultiJointModel中的层次结构 控制各部件旋转角度的变量 示例程序——共用顶点数据&#xff0c;通过模型矩阵缩放实现&#xff08;MultiJointModel.js&#xff09; MultiJointModel.js&#xff08;按键响应部分&#xff09; MultiJointModel.js&#x…

刷题日记——将x减到0的最小操作数

将x减到0的最小操作数 题目链接&#xff1a;https://leetcode.cn/problems/minimum-operations-to-reduce-x-to-zero/ 题目解读 题目要求移除元素总和等于参数x&#xff0c;这道题给我的第一感觉就是从数组的两边入手&#xff0c;对数据进行加和删除&#xff0c;但是这里有一…

滚雪球学Java(24):Java反射

&#x1f3c6;本文收录于「滚雪球学Java」专栏&#xff0c;专业攻坚指数级提升&#xff0c;助你一臂之力&#xff0c;带你早日登顶&#x1f680;&#xff0c;欢迎大家关注&&收藏&#xff01;持续更新中&#xff0c;up&#xff01;up&#xff01;up&#xff01;&#xf…

EasySwipeMenuLayout - 独立的侧滑删除

官网 GitHub - anzaizai/EasySwipeMenuLayout: A sliding menu library not just for recyclerview, but all views. 项目介绍 A sliding menu library not just for recyclerview, but all views. Recommended in conjunction with BaseRecyclerViewAdapterHelper Feature…

TS泛型的使用

函数中使用泛型&#xff1a; function identity<T>(arg: T): T {return arg; }let result identity<number>(10); // 传入number类型&#xff0c;返回number类型 console.log(result); // 输出: 10let value identity<string>(Hello); // 传入string类型&a…

ad18学习笔记十二:如何把同属性的元器件全部高亮?

1、先选择需要修改的器件的其中一个。 2、右键find similar objects&#xff0c;然后在弹出的对话框中&#xff0c;将要修改的属性后的any改为same 3、像这样勾选的话&#xff0c;能把同属性的元器件选中&#xff0c;其他器件颜色不变 注意了&#xff0c;如果这个时候&#xff…

初学phar反序列化

以下内容参考大佬博客&#xff1a;PHP Phar反序列化浅学习 - 跳跳糖 首先了解phar是什么东东 Phar是PHP的压缩文档&#xff0c;是PHP中类似于JAR的一种打包文件。它可以把多个文件存放至同一个文件中&#xff0c;无需解压&#xff0c;PHP就可以进行访问并执行内部语句。 默认开…