多线程——线程池

news2024/11/24 0:42:54

目录

·前言

一、什么是线程池

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 与 y
    private double x;
    private double y;
    // 极坐标系 r 与 a
    private 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 与 y
    private double x;
    private double y;
    // 极坐标系 r 与 a
    private 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() {
            @Override
            public 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 {
    // 创建阻塞队列,用来接收任务,这里设置最多容纳任务量为 100
    private 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 {
        // 创建线程池,指定线程数目为 4
        MyThreadPoolExecutor executor = new MyThreadPoolExecutor(4);
        // 循环 100 次,向线程池中添加 100 个任务
        for (int i = 0; i < 100; i++) {
            int n = i;
            executor.sumbit(new Runnable() {
                @Override
                public void run() {
                    // 任务的内容
                    System.out.println("执行任务:->" + n + ",执行的线程是:->" + Thread.currentThread().getName());
                }
            });
        }
    }
}

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

·结尾

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

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

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

相关文章

GPT-4o 和 GPT-4 Turbo 模型之间的对比

GPT-4o 和 GPT-4 Turbo 之间的对比 备注 要弄 AI &#xff0c;不同模型之间的对比就比较重要。 GPT-4o 是 GPT-4 Turbo 的升级版本&#xff0c;能够提供比 GPT-4 Turbo 更多的内容和信息&#xff0c;但成功相对来说更高一些。 第三方引用 在 2024 年 5 月 13 日&#xff0…

HTB:Blocky[WriteUP]

目录 连接至HTB服务器并启动靶机 使用nmap对靶机进行端口扫描 再次使用nmap对靶机开放端口进行脚本、服务信息扫描 对FTP服务版本&#xff1a;ProFTPD_1.3.5进行漏洞扫描 对SSH服务版本&#xff1a;OpenSSH 7.2p2进行漏洞扫描 使用浏览器访问靶机80端口 使用浏览器访问U…

信息搜集-域名信息收集

1.1 域名信息收集 WHOIS查询&#xff1a; 通过WHOIS查询可以快速得到域名的IP段、DNS解析、注册时间、地址等信息&#xff0c;或许运用合理可以巧妙的绕过CDN。备案信息收集&#xff1a; 网站备案信息收集更加方便定位资产到具体的企业名称、ICP备案号、备案人名称、公司、所处…

图片写入GPS经纬高信息

近期项目中需要往java平台传输图片&#xff0c;直接使用QNetworkAccessManager和QHttpMultipart类即可&#xff0c;其他博文中有分享。 主要是平台接口对所传输图片有要求&#xff1a;需要包含GPS信息&#xff08;经度、纬度、高度&#xff09;。 Qt无法直接实现&#xff0c;…

2003年秋季我给哈工大数学研究生写的讲义

我写的没出版讲义共有278页&#xff0c;书末参考文献 我写的讲义书末索引&#xff0c;冯克勤的书《代数数论》书末没有索引&#xff0c;陆洪文、李云峰的书《模形式讲义》书末也有索引 我写的讲义自制的封面 &#xff08;对数学的研究我的经验是&#xff0c;跟其他科学类似&am…

「Qt Widget中文示例指南」如何实现半透明背景?

Qt 是目前最先进、最完整的跨平台C开发工具。它不仅完全实现了一次编写&#xff0c;所有平台无差别运行&#xff0c;更提供了几乎所有开发过程中需要用到的工具。如今&#xff0c;Qt已被运用于超过70个行业、数千家企业&#xff0c;支持数百万设备及应用。 本文将为大家展示如…

Java应用程序的服务器有哪些

1.Tomcat、Jetty 和 JBoss 区别&#xff1f; Apache Tomcat、Jetty 和 JBoss都是用于部署Java应用程序的服务器&#xff0c;它们都支持Servlet、JSP和其他Java EE&#xff08;现在称为Jakarta EE&#xff09;技术。尽管它们有一些相似的功能&#xff0c;但它们之间还是存在一些…

Mysql在线修改表结构工具gh-ost使用说明及实践

本文内容较多&#xff0c;篇幅较长&#xff0c;若不想了解ghost原理&#xff0c;几种模式的介绍以及具体的验证过程&#xff0c;可直接跳到‘四 gh-ost使用总结’查看简洁版使用说明。 一 gh-ost使用场景 生产环境当有关于一个大表的大操作时(比如select count一个大表)&…

沈阳乐晟睿浩科技有限公司抖音小店领域的强者

在当今数字化浪潮的推动下&#xff0c;电子商务以其便捷性、高效性和广泛的覆盖面&#xff0c;成为了推动经济发展的新引擎。而抖音小店&#xff0c;作为短视频平台上的新兴电商形态&#xff0c;更是凭借其庞大的用户基础、精准的内容推送机制以及独特的购物体验&#xff0c;迅…

qt QNetworkProxy详解

一、概述 QNetworkProxy通过设置代理类型、主机、端口和认证信息&#xff0c;可以使应用程序的所有网络请求通过代理服务器进行。它支持为Qt网络类&#xff08;如QAbstractSocket、QTcpSocket、QUdpSocket、QTcpServer、QNetworkAccessManager等&#xff09;配置网络层代理支持…

Spring Boot框架下的厨艺社交网络构建

1 绪论 1.1 研究背景 现在大家正处于互联网加的时代&#xff0c;这个时代它就是一个信息内容无比丰富&#xff0c;信息处理与管理变得越加高效的网络化的时代&#xff0c;这个时代让大家的生活不仅变得更加地便利化&#xff0c;也让时间变得更加地宝贵化&#xff0c;因为每天的…

iOS 本地存储地址(位置)

前言: UserDefaults 存在沙盒的 Library --> Preferences--> .plist文件 CoreData 存在沙盒的 Library --> Application Support--> xx.sqlite 一个小型数据库里 (注:Application Support 这个文件夹已开始是没有的,只有当你写了存储代码,运行之后,目录里才会出…

IDEA开发工具使用技巧积累

一、IDEA 工具设置默认使用maven的settings.xml文件 第一步&#xff1a;打开idea工具&#xff0c;选中 File ——> New Projects Setup ——> Settings for New Projects 第二步&#xff1a;先设置下自动构建项目这个选项 第三步&#xff1a;选中 Build Tools ——>…

深度学习-循环神经网络-LSTM对序列数据进行预测

项目简介: 使用LSTM模型, 对文本数据进行预测, 每次截取字符20, 对第二十一个字符进行预测, LSTM层: units100, activationrelu Dense层: units输入的文本中的字符种类, 比如我使用的文本有644个不同的字符, 那么units64 激活函数: 因为是多分类, 使用softmax 因为这是最…

树莓派5使用pytorch训练模型(CPU)

Pytorch 对于树莓派提供了较好的支持&#xff0c;可以利用 Pytorch 在树莓派上进行试试推理&#xff0c;当然也可以使用树莓派进行模型训练了&#xff0c;这里尝试使用树莓派CPU对模型进行训练。 0 环境配置 必要的环境安装&#xff0c;这个步骤没有什么值得说的&#xff0c;…

c++(树)

定义 2-3 树中的每一个节点都有两个孩子&#xff08;称为 2 节点&#xff0c;2-node&#xff09;或三个孩子&#xff08;称为 3 节点&#xff0c;3-node)。 2 节点&#xff0c;有一个数据元素和两个孩子。只能有两个孩子或没有孩子&#xff0c;不能出现只有一个孩子的情况。如果…

JVM学习总结:字节码篇

本文是学习尚硅谷宋红康老师主讲的 尚硅谷JVM精讲与GC调优教程 的总结 &#xff0c;部分内容也参考了 JavaGuide 网站&#xff08;文末有链接&#xff09; JVM 概述 Oracle JDK 与 OpenJDK 是什么关系&#xff1f; 2006 年 SUN 公司将 Java 开源&#xff0c;也就有了 OpenJDK。…

【verilog】四位全加器

文章目录 前言一、实验原理二、实验过程三、实验结果参考文献 前言 进行 FPGA 全加器 实验 一、实验原理 module adder(ain,bin,cin,cout,s); input ain,bin,cin; output cout,s; assign coutain&bin | ain&cin | bin&cin; assign sain^bin^cin; endmoduletimesc…

复杂类型map与struct

1.map:Key-Value 型数据格式 建表: create table myhive.test_map( id int, name string, members map<string,string>, age int) row format delimited fields terminated by , COLLECTION ITEMS TERMINATED BY # MAP KEYS TERMINATED BY :; 数据导入:load data local …

基于ssm+jsp的地方疫情管理系统(含源码+数据库)

1.开发环境 开发系统:Windows10/11 架构模式:MVC/前后端分离 JDK版本: Java JDK1.8 开发工具:IDEA 数据库版本: mysql5.7或8.0 数据库可视化工具: navicat 服务器: apache tomcat 主要技术: Java,Spring,SpringMvc,mybatis,mysql,vue 2.视频演示地址 3.功能 该系统包含两个…