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

news2024/9/27 21:21:47

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

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

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

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.coloradmin.cn/o/1026257.html

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

相关文章

基于微信小程序的个人健康管理系统的设计与实现(源码+lw+部署文档+讲解等)

前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战✌&#x1f497; &#x1f447;&#x1f3fb;…

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

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

CSS 布局 (三) 浮动、定位、多列布局

6、浮动 最初用于在文本块内浮动图像&#xff0c;float属性成为在网页上创建多列布局最常用的工具之一。随着flexbox和grid的出现&#xff0c;它现在又回到了最初的目的&#xff0c;正如本文所解释的那样。 6.1 浮动的背景 引入float属性是为了允许web开发人员实现包含图像在…

Qt Charts简介

文章目录 一.图标类型Charts分类1.折线图和样条曲线图2.面积图和散点图3.条形图4.饼图5.误差棒图6.烛台图7.极坐标图 二.坐标轴Axes类型分类三.图例四.图表的互动五.图表样式主题 一.图标类型Charts分类 图表是通过使用系列类的实例并将其添加到QChart或ChartView实例来创建的…

【Linux】指针常量和常量指针

这个是指针常量&#xff0c;不能修改指向【其实就是引用的原型】&#xff1a;可以理解为const是否限制了星号 这个是常量指针&#xff0c;可以改指向&#xff0c;不能改值&#xff1a;

[Qt/C/C++]JSON和程序发布

文章摘于 爱编程的大丙 文章目录 1. JSON1.1 Json数组1.2 Json对象1.3 注意事项 2. Qt中JSON操作2.1 QJsonValue2.2 QJsonObject2.3 QJsonArray2.4 QJsonDocument2.5 举例2.5.1 写文件2.5.2 读文件 3. cjson库的使用3.1 cJSON结构体3.2 cJson API3.2.1 数据的封装3.2.2 Json对…

搭建自动化 Web 页面性能检测系统 —— 设计篇

页面性能对于用户体验、用户留存有着重要影响&#xff0c;当页面加载时间过长时&#xff0c;往往会伴随着一部分用户的流失&#xff0c;也会带来一些用户差评。性能的优劣往往是同类产品中胜出的影响因素&#xff0c;也是一个网站口碑的重要评判标准。 一、名称解释 前端监控…

滚雪球学Java(26):Java进制转换

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

【数据结构】二叉树的节点数,叶子数,第K层节点数,高度,查找x节点,判断是否为完全二叉树等方法

&#x1f490; &#x1f338; &#x1f337; &#x1f340; &#x1f339; &#x1f33b; &#x1f33a; &#x1f341; &#x1f343; &#x1f342; &#x1f33f; &#x1f344;&#x1f35d; &#x1f35b; &#x1f364; &#x1f4c3;个人主页 &#xff1a;阿然成长日记 …

【C++】STL之vector操作

文章目录 简介vector中的成员类型模板参数头文件的包含构造函数vector的访问方式&#xff1a;下标[ ]迭代器范围for 交换swap 简介 vector是stl中的一种数组容器&#xff0c;vector在英文中有矢量的意思&#xff0c;但实际上在数据结构中就是一种类似于数组的结构&#xff1b;…

滚雪球学Java(25):动态代理

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

数据挖掘note(1)

数据挖掘一般分为机器学习和统计学习&#xff0c;大数据学的课程一般是关于机器学习&#xff0c;我们学的浅&#xff0c;主要关于统计学习&#xff0c;示意图如下所示&#xff1a; 这是一个大数据时代&#xff0c;但是数据挖掘的利用率不足0.5%&#xff0c;可见数据挖掘的空间巨…

全国各地演讲口才培训机构信息那么多需要如何选择?

演讲口才是一项非常重要的能力&#xff0c;它不仅可以帮助我们在职场中更好地表达自己&#xff0c;还可以在公共场合中更好地展示自己的个性和魅力。因此&#xff0c;越来越多的人开始关注演讲口才的培训&#xff0c;而全国各地也涌现出了众多的演讲口才培训机构。 选择适合自己…

ReactNative中升级IOS 17版本Crash解决

ReactNative中升级IOS 17版本Crash解决 ReactNative中升级IOS 17版本Crash解决一、问题描述二、原因分析三、解决方案决策3.1 设置宽高为非零值3.2 使用新的UIGraphicsImageRenderer替换就版本的UIGraphicsBeginImageContext 四、可能使用到该API的三方库4.1 react-native-fast…

RK3568开发板SG90 舵机模块的功能实现-迅为电子

1 模块说明 SG90 舵机模块如下图所示: 常见的舵机转向角度有 0-90 度&#xff0c;0-180 度&#xff0c;0-360 度&#xff0c;可以用在垃圾桶项目开盖用&#xff0c;智能小车的全比例转向&#xff0c;摄像头云台&#xff0c;机械臂等。 2 接线说明 SG90 舵机模块上三条线&…

猫头虎博主成为创作者的第1024天纪念日

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

Unity制作曲线进度条

unity制作曲线进度条 大家好&#xff0c;我是阿赵。   在使用Unity引擎做进度条的时候&#xff0c;有时会遇到一个问题&#xff0c;如果进度条不是简单的横向、纵向或者圆形&#xff0c;而是任意的不规则形状&#xff0c;那该怎么办呢&#xff1f;比如这样的&#xff1a; 一…

【JDK 8 -收集和统计】7.1 collector 收集器

一、collect&#xff08;&#xff09;方法 二、两个重载方法 2.1 方法一 2.2 方法二&#xff08;常用&#xff09; 三、Collector 的 作用 四、Collectors 的作用 > Collectors.toList() > Collectors.toMap() > Collectors.toSet() > Collectors.toColl…

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

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

牛客: BM17 二分查找-I

牛客: BM17 二分查找-I 文章目录 牛客: BM17 二分查找-I题目描述题解思路题解代码 题目描述 题解思路 一个左端点, 一个右端点, 循环直到左右端点交叉, 取左右端点的中点 若中点的值大于目标值, 意味着目标值只可能在中点的左边, 所以右端点变为中点的前一个位置 若中点的值…