多线程代码案例(线程池)- 4

目录

引入

标准库中的线程池 -- ThreadPoolExecutor

研究一下这个方法的几个参数

1. int corePoolSize

2. int maximumPoolSize 

3. long keepAliveTime

4. TimeUnit unit

5. BolckingQueue workQueue

6. ThreadFactory threadFactory 

7. RejectedExecutionHandler handler

四种拒绝策略

Executors 工厂类

如何确定线程池的数量???

实现一个简单的线程池


引入

池,是一个非常重要的概念,我们有常量池,数据库连接池,线程池,进程池,内存池....

池的作用:

        1. 提前把要用的对象准备好。

        2. 把用完的对象也不立即释放,先留着以备下次使用 ==》 提高效率!!!

举个栗子: 我是个美女,谈了一个男朋友。如果我有一天对这个男人厌倦了,如何才能提高更换男朋友的效率呢? ==》 备胎池...

最开始,进程能够解决并发编程的问题,之后因为频繁的创建和销毁进程,成本太高了,所以我们引入了轻量级的进程 --> 线程,但如果创建和销毁线程的频率进一步提高,此时线程的创建和销毁的开销,也不能够无视了。(抛开剂量谈毒性,都是耍流氓)我们就需要想办法来优化此处的线程的创建销毁的效率。

        1. 引入轻量级 线程 --> 纤程 / 协程(Java 21 里引入 “虚拟线程” 就是这个东西)。协程本质,是程序员在用户态的代码中进行调度,不是靠内核的调度器调度 ==》节省了很多调度上的开销。(我们在此处不做深入研究...)

        2. 线程池 把要使用的线程提前创建好了,用完了也不直接释放而是以备下次使用。这样就节省了创建 / 销毁 线程的开销。在这个过程中,并没有真的频繁创建销毁线程,只是从线程池里面,取线程使用,用完了就还给线程池。(在这个过程中,会占用比较多的空间,这个代价是无法避免的,可以接收)

那为什么,从线程池里面取线程,就比从系统申请更加高效呢???

举个栗子来说明:还是银行的例子,柜台里面,相当于内核态,大堂相当于用户态。

当我们要办理业务,需要一个身份证复印件的时候,我们并没有带,这时候,柜员就说我们有两个途径取解决:

        1. 自己拿着身份证,去自助复印机上复印即可(纯用户态代码)

        2. 把身份证交给柜员,他拿着身份证帮你去复印。(柜员拿到我们的身份证之后,就消失在我们的视野中了,此时我们无法知道他要花费多长时间,也不知道他都要做那些事情,我们唯一能做的,就是等,等他回来 ~~)

基本的结论:

如果一个工作,滑稽老铁自己就能完成,就更加可控,更加高效。 从线程池里面取线程,就是纯用户代码,就是可控的。

如果一个工作,滑稽老铁要拜托银行的柜员来完成,就不可控,更低效。通过系统申请创建线程,就是需要内核来完成的,不太可控。

标准库中的线程池 -- ThreadPoolExecutor

我们可以在 Java 官方文档中,找到 java.util.concurrent 包,在下面的 Classes 中就可以找到 ThreadPoolExecutor,往下翻可以找到构造方法,有 4 种

我们只需要关注最后一个即可(最后一个的参数是最全的)

研究一下这个方法的几个参数

1. int corePoolSize

表示的是 核心线程数(一个线程池里面,最少得有多少个线程)

2. int maximumPoolSize 

表示的是 最大线程数(一个线程池里面,最多最多能有多少个线程)

注意: 标准库提供的线程池,持有的线程个数,并非是一成不变的,会根据当前任务量,自适应线程的个数。(任务非常多,就多搞几个线程;任务比较少,就少搞几个线程)

举个栗子:假设一个公司,里面有 10 个员工(正式签劳动合同的员工)。当公司的业务非常繁忙的时候,10 个人干不过来了,就需要招聘,一个成本比较低的做法是,招聘实习生(非正式员工),比如可以再招聘 5 个实习生(廉价劳动力)

过了一段时间,公司没那么忙了,大家都闲下来开始摸鱼了,10 个正式员工,是不能被随便裁员的(劳动仲裁~~~)但是这 5 个实习生,是可以随便裁的。把这 5 个实习生裁掉,使当前这 10 个正式员工也没有那么空闲了,整体的成本就降低了。

如果过了一段时间,公司业务又多了起来,10 个人又忙不过来了,此时重新再招几个实习生就好啦~~~

通过实习生,来应对突发的峰值!!!

10 个正式员工,就是核心线程数(参数叫 核心线程数, 而不是 最小线程数)

10 + 5 正式员工 + 实习生 就是最大线程数了

3. long keepAliveTime

表示的是 保持存活时间

4. TimeUnit unit

表示的是 时间单位(s,min,ms,hour)

再用我们的栗子解释:keepAliveTime 就是实习生线程,允许最大的空闲摸鱼时间。即,如果发现某个实习生正在摸鱼(这个线程空闲了),此时要立即马上把这个实习生开除掉吗???不应该!!!可能发生,这边一空闲马上就开除,结果下一时刻,任务又突然多起来了!!!

此处的 keepAliveTime 意思就是实习生线程,空闲时间超过了这个时间阈值,就会被销毁掉。注意:实习生线程被销毁之后,就没有了,在未来的某一天,线程还会重新招聘实习生,但不是之前的那个了

5. BolckingQueue<Runnable> workQueue

这个和定时器是类似的阻塞队列,用来存放等待执行的任务。当核心线程都在忙碌的时候,新任务会被放入这个队列中排队等待。用 Runnable 来作为描述任务的主体。 ==》 也可以设置 PriorityBlockingQueue 带有优先级

6. ThreadFactory threadFactory 

这个表示线程工厂

工厂模式,也是一种常见的设计模式。通过专门的 “工厂类 / 工厂对象”来创建指定的对象~~

工厂模式,本质上是给 Java 语法填坑的 

举个栗子:

我们会发现,上面的代码,无法通过编译~~

在 c++ / Java 中要想提供多个版本的构造方法,就需要让这多个方法能够构成重载

重载的要求:

1. 方法名字相同(构造方法的名字本身都相同)

2. 形参的 个数 / 类型 不同!

上面的代码不符合第二个要求,所以无法通过编译。为了解决上述问题,就引入了 “工厂模式”,使用普通的方法来创建对象,把构造方法封装了一层

如果把工厂方法放到一个其他的类里面,这个其他的类就叫做“工厂类”。

总的来说,通过静态方法封装 new 操作,无需实例化对象,在方法内部设定不同的属性完成对象初始化,构造对象的过程,就是工厂模式。

回过头来说我们的参数:ThreadFactory threadFactory 通过这个工厂类,来创建线程对象(Thread 对象)在这个类里面提供了方法(也不一定非得是静态的)让方法封装 new Thread 的操作,并且给 Thread 设置一些属性,就构成了 ThreadFactory 线程工厂!

7. RejectedExecutionHandler handler

上述参数中,这个是最重要的!!!

这个表示的是拒绝策略。在线程池中,有一个阻塞队列,能够容纳的元素是有上限的。当任务队列已经满了的时候,如果继续往队列里面添加元素,那么线程池会怎么办呢??? ==》 就是这个拒绝策略参数了!

在官方文档中,构造方法的上面就是拒绝策略

四种拒绝策略

1. 继续添加任务,直接抛出异常。此时就是“撂挑子”的状态,新任务 旧任务 都不执行了!!!

2. 新的任务,由添加任务的线程负责执行,此时新的任务会执行,不过并不是线程池执行,而是调用者执行。同事让我帮忙,我自己都忙的焦头烂额,只能忙自己的,同事的忙还得他自己解决。

3. 丢弃最老的任务。将最老的任务舍弃一个,然后执行新的任务。

4. 丢弃最新的任务。直接抛弃新的任务,新的任务就无了,不执行了,调用的线程不会进程, 线程池也不会执行。

Executors 工厂类

ThreadPoolExecutor 本身使用起来还是比较复杂, 因此 Java 标准库还提供了另一个版本,把 ThreadPoolExecutor 给封装了一下~~

Executors 工厂类,通过这个类来创建出不同的线程池对象(在内部把 ThreadPoolExecutor 创建好了并且设置了不同的参数)

我们可以创建一个线程池用如下的方式

可以看到 Executors 这个工厂类中有许多不同的线程池

newSingleThreadExecutor() 是一个定时器类似物,也能延时执行任务

newScheduleThreadPool 是只包含单个线程的线程池

newCachedThreadPool 是线程数目能够动态扩容是线程池

newFixedThreadPool() 是线程数目固定的线程池

示例代码如下:

打印结果如下:

ThreadPoolExecutor 也是通过 submit 添加任务的,只是构造方法不同

什么时候使用 Executors 什么时候使用 ThreadPoolExecutor 呢???

当我们只是简单使用一个线程池的时候,就可以使用 Executors

当我们需要一个高度定制化的线程池的时候,就可以使用 ThreadPoolExecutor

网上流传的 阿里巴巴Java开发编程规范中,写了不建议使用 Executors,一定要使用 ThreadPoolExecutor,用 ThreadPoolExecutor 意味着一切尽在掌握之中,不会出现一些不可控的因素~~ 我们可以参考,但还是要以具体的公司编程规范要求为准啦...

如何确定线程池的数量???

创建线程池的时候,很多时候,需要设定线程池的数量。这个数量应该怎么设置比较合适???我们上面只是随意的设置了一个 4 ,到底怎么样是合适的呢?

网上有很多说法,假如 CPU 的逻辑核心数是 N ,网上的说法:线程数量应该是 N,N + 1,1.5N,2N... ==》 都是错误的。

不同的程序,能够设定的线程的数量是不同的,必须要具体问题具体分析。

要区分,一个线程是 CPU 密集型的任务,还是 IO 密集型的任务。

CPU 密集型的任务:这个线程大部分的时间,都在要 CPU 上运行,进行计算。 比如,在线程 run 里面计算 1 + 2 + ... + 10w 这种就是 CPU 密集型

IO 密集型的任务:这个线程大部分的时间都在等待 IO,不需要去 CPU 上运行,比如,线程 run 里,搞一个 scanner,读取用户的输入,就是 IO 密集型

如果一个进程中,所有的线程都是 CPU 密集型的,每个线程所有的工作都是在 CPU 上执行的(假定的一种极端情况~~~)此时,线程的数目就不应该超过 N(CPU 逻辑核心数)

如果一个进程中,所有的线程都是 IO 密集型的,每个线程的大部分工作都是在等待 IO,CPU 消耗非常少,此时线程的数目就可以很多很多,远远超过 N(CPU 逻辑核心数)

上面的两个场景,是两种非常极端的情况,实际上,一个进程中的线程,一部分是 IO,一份是 CPU,这里的比例是不好确定的。

综上,由于程序的复杂性,很难直接对线程的数量进行估计。更合适的做法应该是:通过实验 / 测试的方式,找到合适的线程数目。==》 尝试给线程池,设定不同的线程数目,分别进行性能测试,衡量每种线程数目下,总的时间开销 和 系统资源占用的开销,找到这两者的合适值。

实现一个简单的线程池

我们这里直接写一个固定数目的线程池,暂时不考虑线程数目的增多和减少。

        1. 提供构造方法,指定创建多少个线程。

        2. 在构造方法中,把这些线程都创建好

        3. 有一个阻塞队列,能够持有要执行的方法

        4. 提供 subbmit 方法,可以添加新的任务。

代码如下:

成员变量有 threadList,用来存储管理线程;queue 用来保存任务的队列,这里因为 ArrayBlockingQueue 是线程安全的,所以在下面的构造方法并没有上锁。在线程中,利用 n 来创建指定个线程。在 while(true) 循环中,线程持续运行,不断从任务队列中 take 任务并 run 执行。

submit 方法:将新的任务添加到队列中

解释:

测试代码:

但是在测试代码中,发现有一个小小的红色波浪线,在 i 下面,为啥会编译报错呢?

==》 变量捕获!!!

run 回调函数访问当前外部作用域的变量就是变量捕获,我们之前讲过,变量捕获的值,要不然是 final 修饰的常量值,要不然是一个“事实 final” 变量,但现在 i 是一直变化的,怎么办呢?

在创建一个 n,把 i 赋值给 n,此处的 n 就是一个“事实 final” 变量,每次循环,都是一个新的 n,n 本身没有改变,就可以被捕获!!!

运行起来,注意,这些多个线程之间的执行顺序的不确定的!!!某个线程获取到了某个任务,但是并非立即执行,这个过程中可能其他线程就到前面执行了。(此处的这些线程,彼此之间都是等价的...)

完整代码如下:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;class MyThreadPoolExecutor {// 用 List 数据结构来存储线程private List<Thread> threadList = new ArrayList<>();// 是一个用来保存任务的队列private BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1000);// 通过 n 指定创建多少个线程public MyThreadPoolExecutor (int n) {for (int i = 0; i < n; i++) {Thread t = new Thread(() -> {// 线程要做的事情是 把任务队列中的任务不停的取出来,并且进行执行while (true) {try {// 此处的 take 是带有阻塞功能的// 如果队列为空,此处的 take 会阻塞Runnable runnable = queue.take();// 取出一个任务执行一个任务runnable.run();} catch (InterruptedException e){e.printStackTrace();}}});t.start();threadList.add(t);}}// 将新任务添加到任务队列里面!public void submit(Runnable runnable) throws InterruptedException {queue.put(runnable);}}
public class ThreadDemo43 {public static void main(String[] args) throws InterruptedException {MyThreadPoolExecutor executor = new MyThreadPoolExecutor(4);for (int i = 0; i < 1000; i++) {int n = i;executor.submit(new Runnable() {@Overridepublic void run() {System.out.println("执行任务 " + n + "当前线程为:+ " + Thread.currentThread().getName());}});}}
}

代码细节完善: 

        1. 我们还可以补充一个关闭线程的方法 shutdown

        2. 补充 shutdown 的成员变量 isShutdown 并且优化捕捉到异常时候的操作

        3. submit 前先进行一个判断:

        4. 打印完毕之后,大约 5s 程序结束

完整改善代码:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;class MyThreadPoolExecutor {// 用 List 数据结构来存储线程private List<Thread> threadList = new ArrayList<>();// 是一个用来保存任务的队列private BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1000);// 线程是否关闭的标志private volatile boolean isShutdown = false;// 通过 n 指定创建多少个线程public MyThreadPoolExecutor (int n) {for (int i = 0; i < n; i++) {Thread t = new Thread(() -> {// 线程要做的事情是 把任务队列中的任务不停的取出来,并且进行执行while (!isShutdown) {try {// 此处的 take 是带有阻塞功能的// 如果队列为空,此处的 take 会阻塞Runnable runnable = queue.take();// 取出一个任务执行一个任务runnable.run();} catch (InterruptedException e){// 当捕捉到 异常的时候,恢复线程的中断状态并退出循环Thread.currentThread().interrupt();break;}}});t.start();threadList.add(t);}}// 将新任务添加到任务队列里面!public void submit(Runnable runnable) throws InterruptedException {if (isShutdown) {throw new IllegalStateException("线程池已经关闭,无法提交新的任务");}queue.put(runnable);}public void shutdown() {isShutdown = true;for (Thread t : threadList) {t.interrupt();}}
}
public class ThreadDemo43 {public static void main(String[] args) throws InterruptedException {MyThreadPoolExecutor executor = new MyThreadPoolExecutor(4);for (int i = 0; i < 1000; i++) {int n = i;executor.submit(new Runnable() {@Overridepublic void run() {System.out.println("执行任务 " + n + "当前线程为:+ " + Thread.currentThread().getName());}});}Thread.sleep(5000);executor.shutdown();}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/75076.shtml

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

相关文章

C,C++,C#

C、C 和 C# 是三种不同的编程语言&#xff0c;虽然它们名称相似&#xff0c;但在设计目标、语法特性、运行环境和应用场景上有显著区别。以下是它们的核心区别&#xff1a; 1. 设计目标和历史 语言诞生时间设计目标特点C1972&#xff08;贝尔实验室&#xff09;面向过程&#…

nginx 代理 https 接口

代码中需要真实访问的接口是&#xff1a;https://sdk2.028lk.com/application-localizationdev.yml文件中配置&#xff1a; url: http:/111.34.80.138:18100/sdk2.028lk.com/该服务器111.34.80.138上 18100端口监听&#xff0c;配置信息为&#xff1a; location /sdk2.028lk.c…

数据结构实验3.1:顺序栈的基本操作与进制转换

文章目录 一&#xff0c;问题描述二&#xff0c;基本要求三&#xff0c;算法分析四&#xff0c;示例代码五&#xff0c;实验操作六&#xff0c;运行效果 一&#xff0c;问题描述 在数据处理中&#xff0c;常常会遇到需要对链接存储的线性表进行操作的情况。本次任务聚焦于将链…

经典频域分析法(Bode图、Nyquist判据) —— 理论、案例与交互式 GUI 实现

目录 经典频域分析法(Bode图、Nyquist判据) —— 理论、案例与交互式 GUI 实现一、引言二、经典频域分析方法的基本原理2.1 Bode 图分析2.2 Nyquist 判据三、数学建模与公式推导3.1 一阶系统的频域响应3.2 多极系统的 Bode 图绘制3.3 Nyquist 判据的数学描述四、经典频域分析…

Vue知识点(5)-- 动画

CSS 动画是 Vue3 中实现组件动画效果的高效方式&#xff0c;主要通过 CSS transitions 和 keyframes 动画 CSS Keyframes&#xff08;关键帧动画&#xff09; 用来创建复杂的动画序列&#xff0c;可以精确控制动画的各个阶段。 核心语法&#xff1a; keyframes animationNa…

小型园区网实验

划分VLAN SW3 [sw3]vlan batch 2 3 20 30 [sw3]interface GigabitEthernet 0/0/1 [sw3-GigabitEthernet0/0/1]port link-type access [sw3-GigabitEthernet0/0/1]port default vlan 2 [sw3-GigabitEthernet0/0/1]int g0/0/2 [sw3-GigabitEthernet0/0/2]port link-type acces…

使用LangChain Agents构建Gradio及Gradio Tools(6)——创建自己的GradioTool

使用LangChain Agents构建Gradio及Gradio Tools(6)——创建自己的GradioTool 本篇摘要16. 使用LangChain Agents构建Gradio及Gradio Tool16.6 创建自己的GradioTool16.6.1 创建步骤16.6.2 创建示例StableDiffusionTool参考文献本章目录如下: 《使用LangChain Agents构建Grad…

SDL显示YUV视频

文章目录 1. **宏定义和初始化**2. **全局变量**3. **refresh_video_timer 函数**4. **WinMain 函数**主要功能及工作流程&#xff1a;总结&#xff1a; 1. 宏定义和初始化 #define REFRESH_EVENT (SDL_USEREVENT 1) // 请求画面刷新事件 #define QUIT_EVENT (SDL…

AnimateCC基础教学:随机抽取花名册,不能重复

一.核心代码: this.btnStartObj.addEventListener("click", switchBtn); this.btnOkObj.addEventListener("click", oKBtn); createjs.Ticker.addEventListener("tick", updateRandom); var _this this; var nameArr ["张三", &quo…

软考 系统架构设计师系列知识点 —— 设计模式之抽象工厂模式

本文内容参考&#xff1a; 软考 系统架构设计师系列知识点之设计模式&#xff08;2&#xff09;_系统架构设计师中考设计模式吗-CSDN博客 https://baike.baidu.com/item/%E6%8A%BD%E8%B1%A1%E5%B7%A5%E5%8E%82%E6%A8%A1%E5%BC%8F/2361182 特此致谢&#xff01; Abstract Fac…

P2040 打开所有的灯

题目背景 pmshz在玩一个益(ruo)智(zhi)的小游戏&#xff0c;目的是打开九盏灯所有的灯&#xff0c;这样的游戏难倒了pmshz。。。 题目描述 这个灯很奇(fan)怪(ren)&#xff0c;点一下就会将这个灯和其周围四盏灯的开关状态全部改变。现在你的任务就是就是告诉pmshz要全部打开…

汉得企业级 PaaS 平台 H-ZERO 1.12.0 发布!四大维度升级,构建企业数字化新底座

汉得企业级 PaaS 平台&#xff08;以下简称"H-ZERO"&#xff09;是一款基于微服务架构的企业级数字化 PaaS 平台&#xff0c;可支持企业各类系统搭建、产品研发&#xff0c;帮助企业快速构架技术中台。 H-ZERO于2025年3月底正式发布 V1.12.0 &#xff0c;此次发布聚…

ReplicaSet、Deployment功能是怎么实现的?

在Kubernetes中&#xff0c;ReplicaSet 和 Deployment 是用于管理 Pod 副本的关键对象。它们各自的功能和实现机制如下&#xff1a; 1. ReplicaSet 功能 管理 Pod 副本&#xff1a;确保指定数量的 Pod 副本一直在运行。如果有 Pod 副本崩溃或被删除&#xff0c;ReplicaSet 会…

物联网外设管理服务平台

1 开发目标 1.1 架构图 操作系统&#xff1a;基于Linux5.10.10源码和STM32MP157开发板&#xff0c;完成tf-a(FSBL)、u-boot(SSBL)、uImage、dtbs的裁剪&#xff1b; 驱动层&#xff1a;为每个外设配置DTS并且单独封装外设驱动模块。其中电压ADC测试&#xff0c;采用linux内核…

PyTorch教程:如何读写张量与模型参数

本文演示了PyTorch中张量&#xff08;Tensor&#xff09;和模型参数的保存与加载方法&#xff0c;并提供完整的代码示例及输出结果&#xff0c;帮助读者快速掌握数据持久化的核心操作。 1. 保存和加载单个张量 通过torch.save和torch.load可以直接保存和读取张量。 import to…

持续集成:GitLab CI/CD 与 Jenkins CI/CD 的全面剖析

一、引言 在当今快速迭代的软件开发领域,持续集成(Continuous Integration,CI)已成为保障软件质量、加速开发流程的关键实践。通过频繁地将代码集成到共享仓库,并自动进行构建和测试,持续集成能够尽早发现并解决代码冲突和缺陷。而 GitLab CI/CD 和 Jenkins CI/CD 作为两…

Python 序列构成的数组(序列的增量赋值)

序列的增量赋值 增量赋值运算符 和 * 的表现取决于它们的第一个操作对象。简单起 见&#xff0c;我们把讨论集中在增量加法&#xff08;&#xff09;上&#xff0c;但是这些概念对 * 和其他 增量运算符来说都是一样的。 背后的特殊方法是 iadd &#xff08;用于“就地加法”&…

GEO, TCGA 等将被禁用?!这40个公开数据库可能要小心使用了

GEO, TCGA 等将被禁用&#xff1f;&#xff01;这40个公开数据库可能要小心使用了 最近NIH公共数据库开始对中国禁用的消息闹得风风火火&#xff1a; 你认为研究者上传到 GEO 数据库上的数据会被禁用吗&#xff1f; 单选 会&#xff0c;毕竟占用存储资源 不会&#xff0c;不…

【如何自建MCP服务器?从协议原理到实践的全流程指南】

文章目录 如何自建MCP服务器&#xff1f;从协议原理到实践的全流程指南一、MCP协议是什么&#xff1f;核心架构 二、为什么要自建MCP服务器&#xff1f;1. 突破LLM的固有局限2. 实现个性化功能扩展3. 确保数据隐私安全 三、手把手搭建MCP服务器&#xff08;Python示例&#xff…

鸿蒙开发_ARKTS快速入门_语法说明_渲染控制---纯血鸿蒙HarmonyOS5.0工作笔记012

然后我们再来看渲染控制 首先看条件渲染,其实就是根据不同的状态,渲染不同的UI界面 比如下面这个暂停 开启播放的 可以看到就是通过if 这种条件语句 修改状态变量的值 然后我们再来看这个, 下面点击哪个,上面横线就让让他显示哪个 去看一下代码 可以看到,有两个状态变量opt…