并发编程 | 线程池的手动创建

news2024/9/22 1:02:32

手动创建线程的弊端

在前面我们讲了手动创建线程的使用方式:当一个任务过来,创建一个线程,线程执行完了任务马上又销毁,然后下一次任务过来又重新创建线程,执行完任务再销毁,周而复始。

那么会导致这样一个问题:反复创建和销毁线程系统开销比较大,比较极端的情况下,如果任务比较简单,那么创建和销毁线程消耗的资源和时间甚至比线程执行任务本身消耗的资源还要大,这样得不偿失。

此外,如果系统中同时提交了n个任务,且每个任务执行的时间比较长,这样系统中就可能同时存在n个线程在执行任务(一个任务一个线程去执行),如果这个n很大,那么过多的线程会占用过多的内存等资源,还会带来过多的上下文切换,同时还会导致系统不稳定。

针对上面的两点问题,线程池出现了。在开发过程中,合理的使用线程池能够带来3个好处:

  • 1.首先是降低资源消耗。通过重复利用已创建的线程降低创建线程和销毁线程所带来的开销。

  • 2.提高响应速度。当任务到达时,任务可以不需要等待线程创建就立即执行。

  • 3.提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅消耗系统资源,同时降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。

手动创建线程池

线程池的创建方式

在 Java 语言中,线程池的创建方式总体来说分为两类:

  • 1.通过ThreadPoolExecutor 手动创建线程池。

  • 2.通过 Executors 执行器自动创建线程池。

本文我们只介绍通过ThreadPoolExecutor如何手动创建我们需要的线程池。

ThreadPoolExecutor类

ThreadPoolExecutor是java线程池最为核心的一个类,它有四个构造方法,我们只看其中最原始的一个构造方法,其余三个都是由它衍生而来:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue                    
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler
                    ) 

可以看到这里有6个参数,我们来看下线程池中各个参数的含义,这些参数直接影响到线程池的效果.

  • corePoolSize:核心线程数,线程池初始化时线程数默认为 0,当有新的任务提交后,会创建新线程执行任务(注意不是在初始化 ThreadPoolExecutor 时立即创建),如果不做特殊设置,此后在程序的运行过程中,线程数通常不会再小于 corePoolSize ,因为它们是核心线程,即便未来可能没有可执行的任务也不会被销毁。

  • maximumPoolSize:线程池允许创建的最大线程数,随着任务量的增加,在核心线程已经全部忙碌时并且任务队列满了之后,线程池会进一步创建新线程,最多可以达到 maximumPoolSize 来应对任务多的场景,如果未来线程有空闲,大于 corePoolSize 的线程会被合理回收。

  • workQueue:任务队列,随着任务的不断增加,线程数会逐渐增加并达到核心线程数,此时如果仍有任务被不断提交,就会被放入 workQueue 任务队列中

  • keepAliveTime和unit:当线程池中线程数量多于核心线程数时,而此时又没有任务可做,线程池就会检测线程的 keepAliveTime,如果超过规定的时间,无事可做的线程就会被销毁,以便减少内存的占用和资源消耗。

  • ThreadFactory:线程工厂,它的作用是生产线程以便执行任务。我们可以选择使用默认的线程工厂,创建的线程都会在同一个线程组,并拥有一样的优先级,且都不是守护线程,我们也可以选择自己定制线程工厂,为线程指定名字、优先级、守护线程等属性。

  • handler:拒绝策略,即当线程池和等待队列都达到最大负荷量时,下一个任务来临时采取的策略。

通过上面介绍可知线程池中的线程创建流程图:

举个栗子:现有一个线程池,corePoolSize=2,maxPoolSize=4,队列长度为2,那么当任务过来会先创建2个核心线程数,接下来进来的任务会进入到队列中直到队列满了,会创建额外的线程来执行任务(最多4个线程),这个时候如果再来任务就会执行拒绝策略。下面看一个例子:

class MyRunnable implements Runnable{
    private int i;
    MyRunnable(int i){
        this.i = i;
    }

    @Override
    public void run() {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yy/MM/dd-HH:mm:ss");
        String time = simpleDateFormat.format(new Date());
        System.out.println(time + " 线程"  + Thread.currentThread().getName() + " 执行任务" + i);
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class ThreadPoolExecutorTest{
    public void testThreadPoolExecutor(){
        ExecutorService executorService = new ThreadPoolExecutor(2, 4, 2, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2));
        for (int i=0; i<10; i++){
            executorService.submit(new MyRunnable(i));
        }
    }
}

public class ThreadPoolExecutorExample {
    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutorTest threadPoolExecutorTest = new ThreadPoolExecutorTest();
        threadPoolExecutorTest.testThreadPoolExecutor();
    }
}

//运行结果
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@179d3b25[Not completed, task = java.util.concurrent.Executors$RunnableAdapter@1a6c5a9e[Wrapped task = thread.threadpool.MyRunnable@37bba400]] rejected from java.util.concurrent.ThreadPoolExecutor@254989ff[Running, pool size = 4, active threads = 4, queued tasks = 2, completed tasks = 0]
    at java.base/java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2055)
    at java.base/java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:825)
    at java.base/java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1355)
    at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:118)
    at thread.threadpool.ThreadPoolExecutorTest.testThreadPoolExecutor(ThreadPoolExecutorExample.java:30)
    at thread.threadpool.ThreadPoolExecutorExample.main(ThreadPoolExecutorExample.java:64)
22/06/25-10:55:41 线程pool-1-thread-4 执行任务5
22/06/25-10:55:41 线程pool-1-thread-3 执行任务4
22/06/25-10:55:41 线程pool-1-thread-1 执行任务0
22/06/25-10:55:41 线程pool-1-thread-2 执行任务1
22/06/25-10:55:43 线程pool-1-thread-2 执行任务2
22/06/25-10:55:43 线程pool-1-thread-4 执行任务3     

在上面的程序中,创建了一个corePoolSize=2,maxPoolSize=4,队列长度为2的线程池,然后向线程池中提交了10个任务,任务中run方法的sleep(2000)模拟任务执行的时间是2s。

通过程序的运行结果可以看出,首先在10:55:41时刻,10个任务先后提交,该时刻马上创建了2个核心线程pool-1-thread-4和pool-1-thread-3立即去执行其中最先提交的两个任务(这里是任务5,4);随后提交的任务2和3则放到任务队列中,暂时不执行;再随后的任务0和1提交的时候,一看2个核心线程还在执行任务而且任务系列已慢,于是马上又创建了2个线程(maxPoolSize-corePoolSize)立即去执行任务0和1;后面还剩4个任务再次提交时,就抛出RejectedExecutionException异常;最后过了2s后也就是在10:55:43 时刻,当有线程已经执行完任务空闲出来于是又从任务队列获取到任务2和3执行。

如何设置线程数

使用线程池,如何合理的给出线程池的大小,是非常重要的。对于线程池的大小不能过大,也不能过小。过大会有大量的线程在相对较少的CPU和内存上竞争,过小又会导致空闲的处理器无法工作,浪费资源,降低吞吐率。对于线程池大小的设定,我们需要考虑的问题有:

  • CPU个数

  • 内存大小

  • 任务类型,是计算密集型(CPU密集型)还是I/O密集型

CPU 密集型任务

所谓的CPU 密集型任务,是指需要大量耗费 CPU 资源的任务,比如加密、解密、压缩、计算等任务;对于这样的任务最佳的线程数为 CPU 核心数的 1~2 倍。

对于CPU 密集型任务,因为计算任务非常重,会占用大量的 CPU 资源,所以这时 CPU 的每个核心工作基本都是满负荷的,如果设置过多的线程,会造成不必要的上下文切换,此时线程数的增多并没有让性能提升,反而由于线程数量过多会导致性能下降。

IO 密集型任务

IO 密集型任务的特点是并不会特别消耗 CPU 资源,但是会把主要时间耗费在IO操作上,比如数据库、文件的读写,网络通信等任务。

对于IO密集型任务最大线程数一般会大于 CPU 核心数很多倍。对于IO密集型任务,因为 IO 读写速度相比于 CPU 的速度而言是很慢的,如果我们设置线程数过少,就可能导致 CPU 资源得不到充分的利用;反之,如果我们设置线程数过多,当所有的任务都在被执行且在等待IO的时候,就会使一部分线程处于闲置状态。

线程数计算通用估算公式

关于线程数目的计算方法,《Java并发编程实战》的作者 Brain Goetz 给出了一个通用的计算方法:

线程数 = CPU 核心数 *1+平均等待时间/平均工作时间)

#例如:假设你的电脑有 4CPU 核心,线程平均等待时间是 200 毫秒,平均工作时间是 800 毫秒。套用公式:

线程数 = 4 * (1 + 200/800) = 4 * 1.25 = 5

**平均等待时间:**在并发程序中,线程并不总是在做计算工作。有时候,它们会等待一些资源,例如等待从磁盘读取数据、等待网络请求的响应,或等待其他线程完成工作。这些等待时间就是“平均等待时间”。

平均工作时间: 线程真正忙于执行计算、处理数据、或者执行逻辑操作的时间。这个时间越长,表示线程越多地利用了 CPU 的处理能力。

由公式可得,线程等待时间所占比例越高,那么你需要更多的线程来“掩盖”这些等待时间,确保在任何时候都有足够的线程在工作,以充分利用 CPU 的处理能力。

线程CPU时间所占比例越高,所需的线程数越少,因为每个线程都会充分利用 CPU。

当然,这里介绍的线程数量估算公司只适用于某些理想化的场景:假设机器上只有一个应用在运行,并且只有一个线程池。

现实中,同一台机器上可能会运行多个不同的应用程序,每个应用程序都有自己的线程池。例如,一台服务器上可能同时运行着 Web 服务器、数据库服务器和缓存服务器。

因此在实际部署时,你需要根据以下几个步骤进行验证和调整:

  • 性能测试:在实际环境中进行压力测试,观察应用程序的性能表现,包括响应时间、吞吐量和 CPU 利用率等。
  • 资源监控:持续监控 CPU、内存、磁盘 I/O 等资源的使用情况,确保线程配置不会导致资源的过度使用或浪费。
  • 动态调整:根据监控结果和性能测试的反馈,动态调整线程池的配置,以达到最佳的资源利用和应用性能。

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

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

相关文章

【数据结构】-----红黑树

目录 前言 一、what is it&#xff1f; 二、How achieve it&#xff1f; Ⅰ、结点类 Ⅱ、实现 插入 情况一&#xff1a;叔叔存在且为红色 情况二&#xff1a;叔叔不存在或者叔叔为黑色 旋转 验证 ①验证中序遍历 ②验证是否满足红黑树的性质 Ⅲ、完整实现代码 三、A…

【课程总结】day22:Qwen模型的体验

前言 在上一章【课程总结】day21&#xff08;下&#xff09;&#xff1a;大模型的三大架构及T5体验中&#xff0c;我们体验了Encoder-Decoder架构的T5模型。本章内容&#xff0c;我们将以Decoder-Only架构的Qwen模型入手&#xff0c;了解Qwen模型结构、聊天模板的概念以及通过…

类型注解-type hint

目录 一、基本介绍 1、为什么需要类型注解 2、类型注解作用和说明 二、变量的类型注解 1、基本语法 2、基本数据类型注解 3、实例对象类型注解 4、容器类型注解 5、容器详细类型注解 6、在注释中使用注解 三、函数(方法)的类型注解 1、基本语法 2、代码演示 四、…

WebService基础学习

一、XML回顾 二、HTTP协议回顾 三、复习准备 四、关于Web Service的几个问题 五、Web Service中的几个重要术语 六、开发webservice 七、WebService面试题

python面向对象三大特征之---封装,私有属性和私有方法,property功能; 继承,重写,object类;多态;类的深拷贝和浅拷贝

1.面向对象三大特征 封装 1.类属性的创建&#xff1a; 2.属性的访问&#xff1a; 私有属性和方法在类外访问的方法也有&#xff1a;不推荐 对象名._类名__私有方法() 对象名._类名__私有属性3.property功能 在Python中&#xff0c;property 是一个内置的功能&#xff0c;它…

jar包在linux无法直接获取resources文件夹下的文件

windows下&#xff0c;通过hutool的FileUtil.file()就可以获取到文件&#xff0c;通过MailUtil.send()将邮件带附件的方式成功&#xff0c;携带附件发邮件。 linux下部署&#xff0c;截图中的FileUtil.file()是拿不到文件的&#xff0c;报IOException while sending message&a…

「团结引擎1.2.0」正式上线!功能全面升级

「团结引擎 1.2.0」来啦&#xff0c;继上次大版本更新又过了三个月&#xff0c;这段时间我们的研发团队一直在收集用户反馈&#xff0c;更新引擎功能。 本次技术更新的内容&#xff0c;涵盖了微信小游戏、团结引擎车机版、OpenHarmony、Audio、Virtual Geometry、Open Euler/A…

开发食堂采购系统源码:优化供应链管理APP的技术路径

当下&#xff0c;开发一个食堂采购系统源码&#xff0c;并将其集成到供应链管理APP中&#xff0c;成为了优化供应链管理的关键路径之一。 一、食堂采购系统的需求分析 食堂采购系统是食堂日常运营中不可或缺的工具&#xff0c;其主要功能包括采购需求管理、供应商管理、订单管…

《中国数据库前世今生》——历史的深度与未来的展望

在探索科技与历史的交织中&#xff0c;我有幸观看了《中国数据库前世今生》这部纪录片。影片开头它不仅是一段技术演进的回顾&#xff0c;更是中国IT领域从跟随到引领的壮丽史诗。后续深刻研读了专家们的深刻讨论&#xff0c;通过这部纪录片&#xff0c;我深刻感受到了数据库技…

PMP–知识卡片--沟通模型

沟通过程中&#xff0c;发送方想把自己的想法传递给接收方&#xff0c;需要先对想法进行编码&#xff0c;将其变成语言或文字&#xff0c;再选择传递的方式&#xff0c;过程中会受到噪声的影响。这里的噪声是广义的&#xff0c;包括所有影响信息传递效果的因素&#xff0c;如杂…

《Ubuntu22.04环境下的ROS2学习笔记2》

一、在ROS2环境下创建功能包 如果您已经完成了上一小节的内容&#xff0c;那么接下来您一定渴望自己创建一个功能包来实现相应的功能。在ROS1中&#xff0c;您创建的功能包可以既写C/C&#xff0c;又写python&#xff0c;但ROS2中不允许用户这么做&#xff0c;您的C/C和python代…

UniApp的神器-开启前端开发的全新篇章

本文介绍了DIYGW UniApp可视化工具作为一款低代码开发平台的特点和优势。该工具采用拖拽式设计和模块化开发&#xff0c;能够快速转化想法为可运行应用&#xff0c;并支持多种平台部署。它具有所见即所得的设计体验、丰富的组件库、前后台通信模块和跨平台兼容性等特点。使用该…

netsat -ano 详解

netsat -ano会输出一大堆端口&#xff0c;为什么nmap扫描出来的却只有两个 因为我们的服务器或者工作站有开启防火墙&#xff0c;过滤了nmap的流量&#xff0c;导致nmap扫描不到一些端口&#xff0c;再加上我们的开放端口有一些是只有本地才能访问的 怎么看哪些端口只有本地能…

基于Pytorch深度学习图像处理基础流程框架(以ResNetGenerator为例)

文章目录 - 模型搭建1. 搭建ResNetGenerator2. 网络实例化3.加载预训练模型权重文件4. 神经网络设置为评估模式 预测处理1. 定义图片的预处理方法2. 导入图片3. 预处理图片4. 调用模型5. 输出结果 - 模型搭建 1. 搭建ResNetGenerator import torch import torch.nn as nnclas…

go 调用C语言函数或者库

1.查看cgo是否开启 go env | grep CGO_ENABLED CGO_ENABLED1 2. go程序中加入 import "C" 通过 import “C” 语句启用 CGO 特性后&#xff0c;CGO 会将上一行代码所处注释块的内容视为 C 代码块 单行注释使用// 多行注释使用/* */ 3. go 与C 类型转换 在g…

HSL模型和HSB模型,和懒人配色的Color Hunt

色彩不仅仅是视觉上的享受&#xff0c;它在数据可视化中也扮演着关键角色。通过合理运用色彩模型&#xff0c;我们可以使数据更具可读性和解释性。在这篇文章将探讨HSL&#xff08;Hue, Saturation, Lightness&#xff09;和HSB&#xff08;Hue, Saturation, Brightness&#x…

【机器学习】深度学习实践

欢迎来到 破晓的历程的 博客 ⛺️不负时光&#xff0c;不负己✈️ 文章目录 引言一、深度学习基础二、图像分类示例三、拓展思考结语 引言 在当今人工智能的浪潮中&#xff0c;深度学习作为其核心驱动力之一&#xff0c;正以前所未有的速度改变着我们的世界。从图像识别、语音…

c语言第18天笔记

构造类型 结构体类型 结构体数组 案例&#xff1a; 需求&#xff1a;对候选人得票的统计程序。设有3个候选人&#xff0c;每次输入一个得票的候选人的名字&#xff0c;要求最后输出 各人得票结果。 ​ /** * 结构体数组案例&#xff1a;对候选人得票的统计程序。设有3个候…

主机组装笔记

参考资源&#xff1a;B站【装机教程】全网最好的装机教程&#xff0c;没有之一&#xff0c;仅供探讨学习 9大部件一览 其中得到固态和机械&#xff0c;是硬盘&#xff0c;存储空间&#xff0c;可以只选固态 CPU&#xff0c;主要有 AMD 和 Intel (AMD&#xff0c;基板的背面布…

力扣 58. 最后一个单词的长度

题目描述 思路 下意识想到先以空格作为分割符对字符串进行分割得到若干个子字符串&#xff0c;然后用字符串长度计算函数计算最后一个子字符串的长度。 该思路代码如下&#xff1a; class Solution:def lengthOfLastWord(self, s: str) -> int:s_array s.split()last_le…