多线程——线程池

目录

·前言

一、什么是线程池

1.引入线程池的原因

2.线程池的介绍

二、标准库中的线程池

1.构造方法

2.方法参数

(1)corePoolSize 与 maximumPoolSize 

(2)keepAliveTime 与 unit

(3)workQueue(任务队列)

(4)threadFactory(线程工厂)

(5) handler(拒绝策略)

3.使用标准库中线程池

三、实现线程池

·结尾


·前言

        在我们学习编程知识过程中一定听说过很多池,比如常量池,还有在我前面 MySql 专栏中 JDBC 编程里提到的数据库连接池,以及本篇文章要为大家介绍的线程池,所谓的这些池作用其实都差不多,都是提前把要用的对象创建好,然后把用完的对象不立即释放留着以备下次使用,这样就可以起到提高效率的作用,本篇文章就会为大家介绍一下什么是线程池,在我们 Java 标准库中的线程池是什么样的,以及使用 Java 代码来实现一个简单的线程池让大家能更清晰的认识线程池,那么就开始本篇文章的介绍内容吧。

一、什么是线程池

1.引入线程池的原因

        在我们最开始,引入进程的概念就能够解决并发编程的问题,后来由于频繁创建销毁进程带来的开销太大,从而引入了线程(轻量级进程)这样的概念,使用复用资源的方式来提高创建销毁的效率,但是如果创建和销毁线程的频率也进一步提高呢?此时,线程的创建和开销也就不能无视了。

        为了优化线程的创建与销毁的效率,有下面两种解决方案:

  1. 引入轻量级线程,也称为“协程”;
  2. 使用线程池。

         为什么协程可以优化线程的开销与销毁,这是因为协程的本质是我们在用户态代码中进行调度,不是靠内核的调度器调度的,这样就可以节省很多调度上的开销,此时,我们代码中创建上千个线程会卡死,但是创建上千个协程就没什么事了。

        虽然协程有很多的好处,但是在 Java 中不是很推荐用上述做法来优化线程的创建与销毁,这是因为引入协程会引入额外的复杂性,使用协程可能不是很稳定,协程的调试比较困难……所以相比于协程,使用线程池对于优化线程的创建与销毁会更好一些,那么下面就进一步介绍一下线程池是什么吧。

2.线程池的介绍

        线程池就是要把使用的线程提前创建好,用完了一个线程也不要直接释放,而是放到线程池中以备下次的使用,这样就节省了线程创建与销毁的开销,因为在这使用线程的过程中,并没有真的频繁创建和销毁线程,只是从线程池中取线程使用,用完还会放回去。

        那么为什么从线程池中取线程就比从系统中申请更高效呢?这就好比你让室友帮你取快递,室友答应帮你取,但是什么时候给你取回来,他在帮你取快递的途中会不会做一些什么事情都是不确定的,相比之下,你自己去取快递,就会更高效,通过上面的例子,我们可以得到以下结论:

  • 从线程池中取线程是纯用户态代码,是可控的;
  • 通过系统申请创建线程,需要系统内核来完成,这是不可控的。

二、标准库中的线程池

1.构造方法

        在 Java 标准库中 ThreadPoolExecutor 这个类就是用来创建线程池的,关于这个类,它的构造方法有很多的参数,由我来给大家介绍一下,下面是这个类的几个构造方法,如下图所示:

        如上图,ThreadPoolExecutor 一共涉及到四个构造方法,这里我只对第四个构造方法的每个参数进行一个介绍,这是因为,最后一个构造方法的参数是最全的,可以这么理解,介绍完第四个构造方法的各个参数,其余三个构造方法也就都包含了。

2.方法参数

(1)corePoolSize 与 maximumPoolSize 

        在标准库提供的线程池中,持有的线程个数并不是一成不变的,它会根据当前的任务量来自适应当前线程的个数(任务数量很多,就会多创建几个线程,任务量比较少,就会少创建几个线程),在构造方法中的前两个参数 int corePoolSize 代表线程池中核心线程数有多少也就是一个线程池中最少得有多少个线程,int maximumPoolSize 代表了线程池中最大线程数是多少也就是一个线程池中最多能有多少个线程。

(2)keepAliveTime 与 unit

        第三个参数:long keepAliveTime 代表的意思就是线程池中除了核心线程外的线程的保持存活时间,在上面介绍了标准库中的线程池是根据当前的任务量来自适应当前线程的个数,这个参数就是自适应实现的一个重要标准,keepAliveTime 可以记录除了核心线程外的线程空闲的时间,如果这些线程的空闲时间超过了 keepAliveTime 的值就会自动销毁这些线程,来达到一个自适应的效果,这里的第四个参数:TimeUnit unit 就是搭配 keepAliveTime 这个参数的,unit 代表的是时间单位,它可以是 s、min、ms、hour……也就代表了空闲时间 keepAliveTime 的时间单位。

(3)workQueue(任务队列)

        第五个参数:BlockingQueue了<Runnable> workQueue 代表线程池中可以有很多个任务,这里使用 Runnable 来作为描述任务的主体,线程池中线程不断从这个阻塞队列中取任务来执行。

(4)threadFactory(线程工厂)

        第六个参数:ThreadFactory threadFactory 意思是线程工厂,通过这个工厂类就可以来创建线程对象(Thread 对象),这个类里提供了方法,方法中封装了 new Thread 这样的操作,并且同时给 Thread 设置了一些属性,由此就构成了 ThreadFactory 线程工厂。 

        这里的线程工厂也用到了一种设计模式:工厂模式,它是通过专门的“工厂类”/“工厂对象”来创建指定对象,那么为什么使用工厂模式呢?我们来看下面的一个代码示例:

// 表示平面上的一个点
class Point {// 笛卡尔坐标系 x 与 yprivate double x;private double y;// 极坐标系 r 与 aprivate double r;private double a;// 通过笛卡尔坐标系来构造这个点public Point(double x, double y) {setX(x);setY(y);}// 通过极坐标系来构造这个点public Point(double r, double a) {setR(r);setA(a);}public double x() {return x;}public void setX(double x) {this.x = x;}public double y() {return y;}public void setY(double y) {this.y = y;}public double r() {return r;}public void setR(double r) {this.r = r;}public double a() {return a;}public void setA(double a) {this.a = a;}
}

        不知道大家看完上面的代码有没有发现什么问题,这里的问题就在于 Point 这个类的两个构造方法不构成重载,它们的参数列表是一样的,如下图所示: 

        想必我们都知道,使用笛卡尔坐标和使用极坐标都可以表示一个点,并且这两个表示方法并不相同,想通过同一个类的构造方法来用这两种不同的方式表示不同的点就违背了 Java 的语法规则,为了解决上述的问题,就引入了“工厂模式”。

        工厂模式的基本逻辑就是使用普通方法来创建对象,在普通方法中把构造方法进行封装,利用工厂模式修改后的代码及运行结果如下所示:

// 表示平面上的一个点
class Point {// 笛卡尔坐标系 x 与 yprivate double x;private double y;// 极坐标系 r 与 aprivate double r;private double a;// 通过笛卡尔坐标系来构造这个点public static Point makePointByXY(double x, double y) {Point point = new Point();point.setX(x);point.setY(y);return point;}// 通过极坐标系来构造这个点public static Point makePointByRA(double r, double a) {Point point = new Point();point.setR(r);point.setA(a);return point;}public double x() {return x;}public void setX(double x) {this.x = x;}public double y() {return y;}public void setY(double y) {this.y = y;}public double r() {return r;}public void setR(double r) {this.r = r;}public double a() {return a;}public void setA(double a) {this.a = a;}
}
public class PointFactory {public static void main(String[] args) {Point point1 = Point.makePointByXY(3,4);Point point2 = Point.makePointByRA(3,30);System.out.println("point1 的笛卡尔坐标:->(" + point1.x() + "," + point1.y() + ")");System.out.println("point2 的极坐标:->(" + point2.r() + "," + point2.a() + ")");}
}

 

         此时,利用工厂模式就可以创建出两个方式表示的点,代码中的 makePointByXY 方法与 makePointByRA 方法也称为工厂方法,如果把工厂方法放到一个其他的类中,这个类就叫做“工厂类”,总的来说,通过静态方法封装 new 操作,在方法内部设定不同的属性来完成对象的初始化,构造对象的过程,就是工厂模式。

(5) handler(拒绝策略)

        第七个参数:RejectedExecutionHandler handler 这个参数可以算是最重要的一个参数,在前面介绍的第五个参数:BlockingQueue了<Runnable> workQueue 这是线程池中的一个阻塞队列,用来存储当前线程池要执行的任务都有哪些,它能够容纳的元素是有上限的,此时当这个阻塞队列中的任务已经排满了,还有新的任务要往这个阻塞队列中添加,线程池该怎么办?这就需要我们的第七个参数:RejectedExecutionHandler handler 来指明一个拒绝策略,如下图所示:

        上图中的这四个类也就代表了四种拒绝策略,它们所对应的拒绝策略如下表所示:

拒绝策略

ThreadPoolExecutor.AbortPolicy

继续添加任务,直接抛出异常。
ThreadPoolExecutor.CallerRunsPolicy新的任务由添加任务的线程负责执行。
ThreadPoolExecutor.DiscardOldestPolicy丢弃最老的任务,添加新的任务。
ThreadPoolExecutor.DiscardPolicy丢弃最新的任务。

3.使用标准库中线程池

        上面介绍了 ThreadPoolExecutor 类的构造方法及构造方法中的参数,可以看出来 ThreadPoolExecutor 类本身用起来比较复杂,因此在标准库中还提供了另一个版本的线程池,也就是把 ThreadPoolExecutor 类给封装了一下,这个线程池就是 Executors 工厂类,通过这个类来创建出的不同线程池对象,Executors 类在内部把 ThreadPoolExecutor 创建好了,并且设置了不同的参数,下面就使用 Executors 演示一下标准库中线程池的效果吧。

        如下图所示,在 Executors 中内置了很多版本的线程池,这里我们使用固定数目的线程池来简单演示一下线程池的效果即可。

        下面使用线程池的具体代码及运行结果如下所示: 

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class TestDemo7 {public static void main(String[] args) {// ExecutorService 提供了一种管理和控制异步任务执行的方式ExecutorService service = Executors.newFixedThreadPool(4);// 使用 submit 方法把任务添加到线程池中service.submit(new Runnable() {@Overridepublic void run() {System.out.println("hello");}});}
}

         介绍完这两个标准库中的线程池,可以明确一点,当我们只是想简单用一下线程池,就可以使用 Executors ,当我们希望高度定制化一个线程池,就可以使用 ThreadPoolExecutor。

三、实现线程池

        在前面介绍了标准库中的线程池及演示了使用的效果,下面我就来写代码实现一个简单的线程池,这里我就直接写一个固定线程数目的线程池,下面是这个简单线程池中包含的内容:

  1. 提供构造方法,指定创建多少个线程;
  2. 在构造方法中,把这些线程都创建好;
  3. 创建一个阻塞队列,能够持有要执行的任务;
  4. 提供 submit 方法,可以添加新的任务。

        那么下面我就直接上代码了,关于这个简单线程池实现的细节我会在代码中以注释的方式进行介绍,线程池实现的代码及运行结果如下所示:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;class MyThreadPoolExecutor {// 创建阻塞队列,用来接收任务,这里设置最多容纳任务量为 100private BlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<>(100);// 创建线程链表,把创建的每个线程都用线程链表组织起来private List<Thread> threadList = new ArrayList<>();// 构造方法,指定线程池中固定的线程数,并且将线程都创建好public MyThreadPoolExecutor(int num) {for (int i = 0; i < num; i++) {Thread t = new Thread(()->{while (true) {// 利用 runnable 来接收阻塞队列中的任务Runnable runnable = null;try {// 获取任务runnable = blockingQueue.take();} catch (InterruptedException e) {throw new RuntimeException(e);}// 执行任务runnable.run();}});// 启动线程t.start();// 将线程加入到线程链表中threadList.add(t);}}// 方法 sumbit 用来向阻塞队列中添加新的任务public void sumbit(Runnable runnable) throws InterruptedException {blockingQueue.put(runnable);}
}public class ThreadDemo8 {public static void main(String[] args) throws InterruptedException {// 创建线程池,指定线程数目为 4MyThreadPoolExecutor executor = new MyThreadPoolExecutor(4);// 循环 100 次,向线程池中添加 100 个任务for (int i = 0; i < 100; i++) {int n = i;executor.sumbit(new Runnable() {@Overridepublic void run() {// 任务的内容System.out.println("执行任务:->" + n + ",执行的线程是:->" + Thread.currentThread().getName());}});}}
}

        如上图的运行结果可以看出,多个线程之间的执行顺序是不确定的,某个线程获取到了某个任务,但并非是立即执行,在这个过程中很有可能另一个线程就插到前面了,这里的这些线程彼此之间都是等价的。

·结尾

        文章到这里就要结束了,回顾本篇文章,我介绍了什么是线程池,标准库中线程池还有实现了一个简单的线程池,其中还是要多理解一下标准库中线程池构造方法每个参数的意思,及理解拒绝策略的含义,这可以让我们对 ThreadPoolExecutor 类的使用更加清晰,后面实现的线程池也就可以看出线程池基本的工作原理,那就是不断利用这 4 个线程来执行任务,这样就省去创建和销毁线程的开销,那么如果你感觉本篇文章对你有所帮助,还是希望能收到你的三连鼓励,如果对文章的内容有所疑问欢迎在评论区进行讨论,我们下一篇文章再见吧~~~

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

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

相关文章

Redis集群分片存储最佳实践,手把手搭建Redis集群

目录 一、Redis集群介绍1、设计目标2、为什么需要分片存储 二、官网集群方案三、搭建集群1、准备6个独立的Redis服务2、通过redis-cli工具创建集群3、检验集群4、集群重新分片5、故障转移测试6、集群扩容7、集群节点删除 四、集群关心的问题五、集群功能限制 一、Redis集群介绍…

蓝桥杯题目理解

1. 一维差分 1.1. 小蓝的操作 1.1.1. 题目解析&#xff1a; 这道题提到了对于“区间”进行操作&#xff0c;而差分数列就是对于区间进行操作的好方法。 观察差分数列&#xff1a; 给定数列&#xff1a;1 3 5 2 7 1 差分数列&#xff1a;1 2 2 -3 5 6 题目要求把原数组全部…

基于SpringBoot的高校体测管理系统设计与实现(源码+定制+开发)高校体测记录系统设计、高校体测信息管理平台、智能体测管理系统开发、高校体测记录系统设计

博主介绍&#xff1a; ✌我是阿龙&#xff0c;一名专注于Java技术领域的程序员&#xff0c;全网拥有10W粉丝。作为CSDN特邀作者、博客专家、新星计划导师&#xff0c;我在计算机毕业设计开发方面积累了丰富的经验。同时&#xff0c;我也是掘金、华为云、阿里云、InfoQ等平台…

25届电信保研经验贴(自动化所)

个人背景 学校&#xff1a;中九 专业&#xff1a;电子信息工程 加权&#xff1a;92.89 绩点&#xff1a;3.91/4.0 rank&#xff1a;前五学期rank2/95&#xff0c;综合排名rank1&#xff08;前六学期和综合排名出的晚&#xff0c;实际上只用到了前五学期&#xff09; 科研…

海外云手机实现高效的海外社交媒体营销

随着全球化的深入发展&#xff0c;越来越多的中国企业走向国际市场&#xff0c;尤其是B2B外贸企业&#xff0c;海外社交媒体营销已成为其扩大市场的重要手段。在复杂多变的海外市场环境中&#xff0c;如何有效提高营销效率并降低运营风险&#xff0c;成为了众多企业的首要任务。…

路由器 相关知识

一、路由器是什么 参考&#xff1a;图解系列--路由器和它庞大的功能_路由功能-CSDN博客 路由器是指&#xff1a;主要负责 OSI参考模型中网络层的处理工作&#xff0c;并根据路由表信息在不同的网络 之间转发IP 分组的网络硬件(图3-1)。这里的网络一般是指IP 子网&#xff0c;…

Java基础(7)图书管理系统

目录 1.前言 2.正文 2.1思路 2.2Book包 2.3people包 2.4operation包 2.5主函数 3.小结 1.前言 哈喽大家好吖&#xff0c;今天来给前面Java基础的学习来一个基础的实战&#xff0c;做一个简单的图书管理系统&#xff0c;这里边综合利用了我们之前学习到的类和对象&…

爬虫ip技术未来发展趋势

各位朋友&#xff0c;大家好&#xff01;有伙伴问爬虫技术未来会有更好的发展么&#xff0c;那今天小蝌蚪来跟大家聊聊爬虫技术未来的发展趋势分享一下行业咨询。 大家在日常工作和生活中&#xff0c;都希望事情能更省心、高效吧&#xff1f;未来的爬虫技术就朝着这个方向发展…

sheng的学习笔记-AI基础-正确率/召回率/F1指标/ROC曲线

AI目录&#xff1a;sheng的学习笔记-AI目录-CSDN博客 分类准确度问题 假设有一个癌症预测系统&#xff0c;输入体检信息&#xff0c;可以判断是否有癌症。如果癌症产生的概率只有0.1%&#xff0c;那么系统预测所有人都是健康&#xff0c;即可达到99.9%的准确率。 但显然这样的…

在Keil调试内存中的程序

在Keil调试内存中的程序 目录 在Keil调试内存中的程序1. 问题引出2. 测试工程3. 工程和Keil配置 实验环境&#xff1a; MCU&#xff1a;STM32F103C8T6 (Flash 64K RAM 20K)Keil&#xff1a;uVision V5.27.0.0仿真器&#xff1a;ST-Link 参考源码&#xff1a;https://download.c…

Redis 集群 总结

前言 相关系列 《Redis & 目录》&#xff08;持续更新&#xff09;《Redis & 集群 & 源码》&#xff08;学习过程/多有漏误/仅作参考/不再更新&#xff09;《Redis & 集群 & 总结》&#xff08;学习总结/最新最准/持续更新&#xff09;《Redis & 集群…

导出问题处理

问题描述 测试出来一个问题&#xff0c;使用地市的角色&#xff0c;导出数据然后超过了20w的数据&#xff0c;提示报错&#xff0c;我还以为是偶然的问题&#xff0c;然后是发现是普遍的问题&#xff0c;本地环境复现了&#xff0c;然后是&#xff0c;这个功能是三套角色&…

ESP32-S3学习笔记:常用的ESP-IDF命令总结

参考资料&#xff1a;1.esptool.py工具 2.idf.py工具 后续文章的讲解需要用到IDF命令行工具&#xff0c;当前文章简单介绍一下。 目录 打开命令行的小技巧 一、读flash信息 二、擦除flash 三、读flash数据 四、写flash数据 打开命令行的小技巧 大家安装完IDF开发包后…

React类组件详解

React类组件是通过创建class继承React.Component来创建的&#xff0c;是React中用于构建用户界面的重要部分。以下是对React类组件的详细解释&#xff1a; 一、定义与基本结构 类组件使用ES6的class语法定义&#xff0c;并继承自React.Component。它们具有更复杂的功能&#xf…

腾讯云 COS 多 AZ 存储保证服务高可用性

腾讯云 COS 的多 AZ 存储架构能够为用户数据提供数据中心级别的容灾能力。多 AZ 存储将客户数据分散存储在城市中多个不同的数据中心&#xff0c;当某个数据中心因为自然灾害、断电等极端情况导致整体故障时&#xff0c;多 AZ 存储架构依然可以为客户提供稳定可靠的存储服务。 …

表格编辑demo

<el-form :model"form" :rules"status ? rules : {}" ref"form" class"form-container" :inline"true"><el-table :data"tableData"><el-table-column label"计算公式"><templat…

ArcGIS001:ArcGIS10.2安装教程

摘要&#xff1a;本文详细介绍arcgis10.2的安装、破解、汉化过程。 一、软件下载 安装包链接&#xff1a;https://pan.baidu.com/s/1T3UJ7t_ELZ73TH2wGOcfpg?pwd08zk 提取码&#xff1a;08zk 二、安装NET Framework 3.5 双击打开控制面板&#xff0c;点击【卸载程序】&…

05方差分析续

文章目录 1.Three way ANOVA2.Latin square design2.Hierarchical (nested) ANOVA3.Split-plot ANOVA4.Repeated measures ANOVA5.Mixed effect models 1.Three way ANOVA 三因素相关分析 单因子分析的代码 data(mtcars) nrow(mtcars) # 32 mtcars$cyl as.factor(mtcars$cyl…

c#子控件拖动父控件方法及父控件限在窗体内拖动

一、效果 拖放位置不超过窗体四边,超出后自动靠边停靠支持多子控件拖动指定控件拖放(含父控件或窗体)点击左上角logo弹出消息窗口(默认位置右下角)1.1 效果展示 1.2 关于MQTTnet(最新版v4.3.7.1207)实现在线客服功能,见下篇博文 https://github.com/dotnet/MQTTnet 网上…

BIO,NIO,直接内存,零拷贝

前置知识 什么是Socket&#xff1f; Socket是应用层与TCP/IP协议族通信的中间软件抽象层&#xff0c;它是一组接口&#xff0c;一般由操作系统提供。在设计模式中&#xff0c;Socket其实就是一个门面模式&#xff0c;它把复杂的TCP/IP协议处理和通信缓存管理等等都隐藏在Sock…