7000字+24张图带你彻底弄懂线程池

图片

大家好,我是三友。今天跟大家聊一聊无论是在工作中常用还是在面试中常问的线程池,通过画图的方式来彻底弄懂线程池的工作原理,以及在实际项目中该如何自定义适合业务的线程池。

一、什么是线程池

线程池其实是一种池化的技术的实现,池化技术的核心思想其实就是实现资源的一个复用,避免资源的重复创建和销毁带来的性能开销。在线程池中,线程池可以管理一堆线程,让线程执行完任务之后不会进行销毁,而是继续去处理其它线程已经提交的任务。

使用线程池的好处

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

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

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

二、线程池的构造

Java中主要是通过构建ThreadPoolExecutor来创建线程池的。接下来我们看一下线程池是如何构造出来的

ThreadPoolExecutor的构造方法

图片

  • corePoolSize:线程池中用来工作的核心的线程数量。

  • maximumPoolSize:最大线程数,线程池允许创建的最大线程数。

  • keepAliveTime:超出 corePoolSize 后创建的线程存活时间或者是所有线程最大存活时间,取决于配置。

  • unit:keepAliveTime 的时间单位。

  • workQueue:任务队列,是一个阻塞队列,当线程数已达到核心线程数,会将任务存储在阻塞队列中。

  • threadFactory :线程池内部创建线程所用的工厂。

  • handler:拒绝策略;当队列已满并且线程数量达到最大线程数量时,会调用该方法处理该任务。

线程池的构造其实很简单,就是传入一堆参数,然后进行简单的赋值操作。

三、线程池的运行原理

说完线程池的核心构造参数的意思,接下来就来画图讲解这些参数在线程池中是如何工作的。

线程池刚创建出来是什么样子呢,如下图

图片

不错,刚创建出来的线程池中只有一个构造时传入的阻塞队列而已,此时里面并没有的任何线程,但是如果你想要在执行之前已经创建好核心线程数,可以调用prestartAllCoreThreads方法来实现,默认是没有线程的。

当有线程通过execute方法提交了一个任务,会发生什么呢?

提交任务的时候,其实会去进行任务的处理

首先会去判断当前线程池的线程数是否小于核心线程数,也就是线程池构造时传入的参数corePoolSize。

如果小于,那么就直接通过ThreadFactory创建一个线程来执行这个任务,如图

图片

当任务执行完之后,线程不会退出,而是会去从阻塞队列中获取任务,如下图

图片

接下来如果又提交了一个任务,也会按照上述的步骤,去判断是否小于核心线程数,如果小于,还是会创建线程来执行任务,执行完之后也会从阻塞队列中获取任务。这里有个细节,就是提交任务的时候,就算有线程池里的线程从阻塞队列中获取不到任务,如果线程池里的线程数还是小于核心线程数,那么依然会继续创建线程,而不是复用已有的线程。

如果线程池里的线程数不再小于核心线程数呢?那么此时就会尝试将任务放入阻塞队列中,入队成功之后,如图

图片

这样在阻塞的线程就可以获取到任务了。

但是,随着任务越来越多,队列已经满了,任务放入失败了,那怎么办呢?

此时就会判断当前线程池里的线程数是否小于最大线程数,也就是入参时的maximumPoolSize参数

如果小于最大线程数,那么也会创建非核心线程来执行提交的任务,如图

图片

所以,从这里可以发现,就算队列中有任务,新创建的线程还是优先处理这个提交的任务,而不是从队列中获取已有的任务执行,从这可以看出,先提交的任务不一定先执行。

但是不幸的事发生了,线程数已经达到了最大线程数量,那么此时会怎么办呢?

此时就会执行拒绝策略,也就是构造线程池的时候,传入的RejectedExecutionHandler对象,来处理这个任务。

图片

RejectedExecutionHandler的实现JDK自带的默认有4种

  • AbortPolicy:丢弃任务,抛出运行时异常

  • CallerRunsPolicy:由提交任务的线程来执行任务

  • DiscardPolicy:丢弃这个任务,但是不抛异常

  • DiscardOldestPolicy:从队列中剔除最先进入队列的任务,然后再次提交任务

线程池创建的时候,如果不指定拒绝策略就默认是AbortPolicy策略。当然,你也可以自己实现RejectedExecutionHandler接口,比如将任务存在数据库或者缓存中,这样就数据库或者缓存中获取到被拒绝掉的任务了。

到这里,我们发现,线程池构造的几个参数corePoolSize、maximumPoolSize、workQueue、threadFactory、handler我们都在上述的执行过程中讲到了,那么还差两个参数keepAliveTime和unit(unit是keepAliveTime的时间单位)没讲到,所以keepAliveTime是如何起到作用的呢,这个问题留到后面分析。

说完整个执行的流程,接下来看看execute方法代码是如何实现的。

图片

  • workerCountOf(c)<corePoolSize:这行代码就是判断是否小于核心线程数,是的话就通过addWorker方法,addWorker就是添加线程来执行任务。

  • workQueue.offer(command):这行代码就表示尝试往阻塞队列中添加任务

  • 添加失败之后就会再次调用addWorker方法尝试添加非核心线程来执行任务

  • 如果还是添加非核心线程失败了,那么就会调用reject(command)来拒绝这个任务。

最后再来另画一张图总结execute执行流程

图片

四、线程池中线程实现复用的原理

线程池的核心功能就是实现了线程的重复利用,那么线程池是如何实现线程的复用呢?

线程在线程池内部其实是被封装成一个Worker对象

图片

Worker继承了AQS,也就是有一定锁的特性。

创建线程来执行任务的方法上面提到是通过addWorker方法创建的。在创建Worker对象的时候,会把线程和任务一起封装到Worker内部,然后调用runWorker方法来让线程执行任务,接下来我们就来看一下runWorker方法。

图片

从这张图可以看出线程执行完任务不会退出的原因,runWorker内部使用了while死循环,当第一个任务执行完之后,会不断地通过getTask方法获取任务,只要能获取到任务,就会调用run方法,继续执行任务,这就是线程能够复用的主要原因。

但是如果从getTask获取不到方法的时候,最后就会调用finally中的processWorkerExit方法,来将线程退出。

这里有个一个细节就是,因为Worker继承了AQS,每次在执行任务之前都会调用Worker的lock方法,执行完任务之后,会调用unlock方法,这样做的目的就可以通过Woker的加锁状态就能判断出当前线程是否正在运行任务。如果想知道线程是否正在运行任务,只需要调用Woker的tryLock方法,根据是否加锁成功就能判断,加锁成功说明当前线程没有加锁,也就没有执行任务了,在调用shutdown方法关闭线程池的时候,就用这种方式来判断线程有没有在执行任务,如果没有的话,来尝试打断没有执行任务的线程。

五、线程是如何获取任务的以及如何实现超时的

上一节我们说到,线程在执行完任务之后,会继续从getTask方法中获取任务,获取不到就会退出。接下来我们就来看一看getTask方法的实现。

图片

getTask方法,前面就是线程池的一些状态的判断,这里有一行代码

boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

这行代码是判断,当前过来获取任务的线程是否可以超时退出。如果allowCoreThreadTimeOut设置为true或者线程池当前的线程数大于核心线程数,也就是corePoolSize,那么该获取任务的线程就可以超时退出。

那是怎么做到超时退出呢,就是这行核心代码

Runnable r = timed ?                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :                    workQueue.take();

会根据是否允许超时来选择调用阻塞队列workQueue的poll方法或者take方法。如果允许超时,则会调用poll方法,传入keepAliveTime,也就是构造线程池时传入的空闲时间,这个方法的意思就是从队列中阻塞keepAliveTime时间来获取任务,获取不到就会返回null;如果不允许超时,就会调用take方法,这个方法会一直阻塞获取任务,直到从队列中获取到任务位置。从这里可以看到keepAliveTime是如何使用的了。

所以到这里应该就知道线程池中的线程为什么可以做到空闲一定时间就退出了吧。其实最主要的是利用了阻塞队列的poll方法的实现,这个方法可以指定超时时间,一旦线程达到了keepAliveTime还没有获取到任务,那么就会返回null,上一小节提到,getTask方法返回null,线程就会退出。

这里也有一个细节,就是判断当前获取任务的线程是否可以超时退出的时候,如果将allowCoreThreadTimeOut设置为true,那么所有线程走到这个timed都是true,那么所有的线程,包括核心线程都可以做到超时退出。如果你的线程池需要将核心线程超时退出,那么可以通过allowCoreThreadTimeOut方法将allowCoreThreadTimeOut变量设置为true。

整个getTask方法以及线程超时退出的机制如图所示

图片

六、线程池的5种状态

线程池内部有5个常量来代表线程池的五种状态

图片

  • RUNNING:线程池创建时就是这个状态,能够接收新任务,以及对已添加的任务进行处理。

  • SHUTDOWN:调用shutdown方法线程池就会转换成SHUTDOWN状态,此时线程池不再接收新任务,但能继续处理已添加的任务到队列中任务。

  • STOP:调用shutdownNow方法线程池就会转换成STOP状态,不接收新任务,也不能继续处理已添加的任务到队列中任务,并且会尝试中断正在处理的任务的线程。

  • TIDYING:

    SHUTDOWN 状态下,任务数为 0, 其他所有任务已终止,线程池会变为 TIDYING 状态。

    线程池在 SHUTDOWN 状态,任务队列为空且执行中任务为空,线程池会变为 TIDYING 状态。

    线程池在 STOP 状态,线程池中执行中任务为空时,线程池会变为 TIDYING 状态。

  • TERMINATED:线程池彻底终止。线程池在 TIDYING 状态执行完 terminated() 方法就会转变为 TERMINATED 状态。

线程池状态具体是存在ctl成员变量中,ctl中不仅存储了线程池的状态还存储了当前线程池中线程数的大小

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

最后画个图来总结一下这5种状态的流转

图片

其实,在线程池运行过程中,绝大多数操作执行前都得判断当前线程池处于哪种状态,再来决定是否继续执行该操作。

七、线程池的关闭

线程池提供了shutdown和shutdownNow两个方法来关闭线程池。

shutdown方法

图片

就是将线程池的状态修改为SHUTDOWN,然后尝试打断空闲的线程(如何判断空闲,上面在说Worker继承AQS的时候说过),也就是在阻塞等待任务的线程。

shutdownNow方法

图片

就是将线程池的状态修改为STOP,然后尝试打断所有的线程,从阻塞队列中移除剩余的任务,这也是为什么shutdownNow不能执行剩余任务的原因。

所以也可以看出shutdown方法和shutdownNow方法的主要区别就是,shutdown之后还能处理在队列中的任务,shutdownNow直接就将任务从队列中移除,线程池里的线程就不再处理了。

八、线程池的监控

在项目中使用线程池的时候,一般需要对线程池进行监控,方便出问题的时候进行查看。线程池本身提供了一些方法来获取线程池的运行状态。

  • getCompletedTaskCount:已经执行完成的任务数量

  • getLargestPoolSize:线程池里曾经创建过的最大的线程数量。这个主要是用来判断线程是否满过。

  • getActiveCount:获取正在执行任务的线程数据

  • getPoolSize:获取当前线程池中线程数量的大小

除了线程池提供的上述已经实现的方法,同时线程池也预留了很对扩展方法。比如在runWorker方法里面,在执行任务之前会回调beforeExecute方法,执行任务之后会回调afterExecute方法,而这些方法默认都是空实现,你可以自己继承ThreadPoolExecutor来扩展重写这些方法,来实现自己想要的功能。

九、Executors构建线程池以及问题分析

JDK内部提供了Executors这个工具类,来快速的创建线程池。

1)固定线程数量的线程池:核心线程数与最大线程数相等

图片


2)单个线程数量的线程池

图片

3)接近无限大线程数量的线程池

图片

4)带定时调度功能的线程池

图片

虽然JDK提供了快速创建线程池的方法,但是其实不推荐使用Executors来创建线程池,因为从上面构造线程池可以看出,newFixedThreadPool线程池,由于使用了LinkedBlockingQueue,队列的容量默认是无限大,实际使用中出现任务过多时会导致内存溢出;newCachedThreadPool线程池由于核心线程数无限大,当任务过多的时候,会导致创建大量的线程,可能机器负载过高,可能会导致服务宕机。

十、线程池的使用场景

在java程序中,其实经常需要用到多线程来处理一些业务,但是不建议单纯使用继承Thread或者实现Runnable接口的方式来创建线程,那样就会导致频繁创建及销毁线程,同时创建过多的线程也可能引发资源耗尽的风险。所以在这种情况下,使用线程池是一种更合理的选择,方便管理任务,实现了线程的重复利用。所以线程池一般适合那种需要异步或者多线程处理任务的场景。

十一、实际项目中如何合理的自定义线程池

通过上面分析提到,通过Executors这个工具类来创建的线程池其实都无法满足实际的使用场景,那么在实际的项目中,到底该如何构造线程池呢,该如何合理的设置参数?

1)线程数

线程数的设置主要取决于业务是IO密集型还是CPU密集型。

CPU密集型指的是任务主要使用来进行大量的计算,没有什么导致线程阻塞。一般这种场景的线程数设置为CPU核心数+1。

IO密集型:当执行任务需要大量的io,比如磁盘io,网络io,可能会存在大量的阻塞,所以在IO密集型任务中使用多线程可以大大地加速任务的处理。一般线程数设置为 2*CPU核心数

java中用来获取CPU核心数的方法是:Runtime.getRuntime().availableProcessors();


2)线程工厂

一般建议自定义线程工厂,构建线程的时候设置线程的名称,这样就在查日志的时候就方便知道是哪个线程执行的代码。

3)有界队列

一般需要设置有界队列的大小,比如LinkedBlockingQueue在构造的时候就可以传入参数,来限制队列中任务数据的大小,这样就不会因为无限往队列中扔任务导致系统的oom。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

python-爬虫(可直接使用)

爬虫&#xff08;Web Scraping&#xff09;是指通过编程自动化地获取互联网上的信息的过程。爬虫的目的通常是从网页中抓取数据&#xff0c;进行数据分析、处理或展示。以下是爬虫的基本流程和一些重要的概念&#xff1a; 爬虫基本流程&#xff1a; 确定目标&#xff1a; 确定要…

Adversarial Attack and Defense on Graph Data: A Survey(2022 IEEE Trans)

Adversarial Attack and Defense on Graph Data: A Survey----《图数据的对抗性攻击和防御&#xff1a;综述》 图对抗攻击论文数据库&#xff1a; https://github.com/safe-graph/graph-adversarial-learning-literature 摘要 深度神经网络&#xff08;DNN&#xff09;已广泛应…

图书管理系统源码,图书管理系统开发,图书借阅系统源码四TuShuManager应用程序MVC控制器Controllers

Asp.net web应用程序MVC之Controllers控制器 Controller在ASP.NET MVC中负责控制所有客户端与服务器端的交互,并且负责协调Model与View之间的数据传递,是ASP.NET MVC的核心。 撰写Controller的基本要求: 1、Controller必须为公开类别; 2、Controller名称必须以Controller结…

初识前后端数据交互(新手篇)

一个软件项目的开发必然是离不开前端和后端的协作&#xff0c;对于刚入行的新手前端或者新手后端来说&#xff0c;很有必要了解一下对方是在做什么&#xff0c;以及提供给自己什么样的帮助&#xff0c;为什么需要对方共同协作才能完成整个软件项目的开发呢&#xff1f;希望这篇…

14.1 USA.gov Data from Bitly(USA.gov数据集)

CHAPTER 14 Data Analysis Examples&#xff08;数据分析实例&#xff09; 14.1 USA.gov Data from Bitly&#xff08;USA.gov数据集&#xff09; 2011年&#xff0c;短链接服务&#xff08;URL shortening service&#xff09;商Bitly和美国政府网站USA.gov合作&#xff0c;…

【springboot】宝塔简单部署springboot 配置https

宝塔简单部署springboot配置https 需求步骤1. springboot通过maven组件打成jar包2. 将jar包部署到宝塔上3. 下载安装nginx并创建网站节点4. 设置域名或者IP5. 设置反向代理:代理后端服务的ip和端口7. 配置SSL/TLS 需求 宝塔部署springboot项目,用nginx反向代理后端IP端口&…

深入理解OS--数值编码

信息的表示和处理 寻址和字节顺序 位于0x100处&#xff0c;int类型值0x01234567在大端和小端下的存储。 字符串的存储不受字节序影响。 移位 1.对左移&#xff0c;右边统一补0 2.对右移&#xff0c;分为算术右移&#xff0c;逻辑右移 算术右移下&#xff0c;左边补原最高有效…

VS2022 配置Qt编译环境 | winows安装Qt5.14.2 | VS2017和Qt5配置成功指南

Visual Studio 2022安装教程完文本内容较多,请耐心看完,挺有收获的,要自己多尝试哦。 文章目录 # 插件安装 如果你想用VS2022来创建QT项目,那么你首先要学会下面的操作,创建一个空白解决方案,在扩展搜索qt,并且下载两个插件(带有绿√的就是)。这里其实是一个坑:VS20…

万宾科技第四代可燃气体监测仪的作用

燃气作为一种重要的能源已在居民生活、工业生产和商业活动等领域得到了广泛的应用。但是与之而来的便是各种各样的燃气管网的安全问题&#xff0c;其中燃气管网泄漏成为了城市生命线建设中亟待解决的安全隐患。因此采取切实有效的措施来保障燃气管网的安全运行&#xff0c;应用…

NB-IoT BC260Y Open CPU SDK④开发环境搭建

NB-IoT BC260Y Open CPU SDK④开发环境搭建 1、SDK包的介绍2、编程工具3、程序框架1、SDK包的介绍 (1)、SDK包的下载: 链接: (2)、文件目录介绍 文件名描述device启动文件、底层配置文档等doc存放 QuecOpen 项目相关的说明文档osFreeRTOS 相关代码out输出编译 App 和调…

【Python】遍历电脑中的所有文件

通过os模块中的os.walk()遍历电脑指定路径的所有文件及大小&#xff1a; import osdef traverse_files(path):file_path_list[]file_size_list[]for root, dirs, files in os.walk(path):for file in files:file_path os.path.join(root, file)file_path_list.append(file_pa…

最新版小权云黑系统 骗子添加查询源码

小权云黑系统添加骗子&#xff0c;查询骗子&#xff0c;可添加团队后台方便审核用&#xff0c;在线反馈留言系统&#xff0c;前台提交骗子&#xff0c;后台需要审核才能过&#xff0c;后台使用光年UI界面&#xff0c;新增导航列表&#xff0c;可给网站添加导航友链&#xff0c;…

爬虫系统Docker和Kubernetes部署运维最佳实践

在构建和管理爬虫系统时&#xff0c;使用Docker和Kubernetes可以带来诸多好处&#xff0c;如方便的部署、弹性伸缩和高可靠性。然而&#xff0c;正确的部署和运维实践对于确保系统稳定运行至关重要。在本文中&#xff0c;我将分享爬虫系统在Docker和Kubernetes上的最佳部署和运…

2020年09月 Scratch图形化(四级)真题解析#中国电子学会#全国青少年软件编程等级考试

Scratch等级考试(1~4级)全部真题・点这里 一、单选题(共15题,每题2分,共30分) 第1题 执行下面程序,输入4和7后,角色说出的内容是? A:4,7 B:7,7 C:7,4 D:4,4 答案:B 第2题 执行下面程序,输出是? A:大学 中庸 孟子 论语 B:论语 大学 孟子 中庸 C:大…

Unity中Shader的BRDF解析(一)

文章目录 前言现在我们主要来看Standard的 漫反射 和 镜面反射一、PBS的核心计算BRDF二、Standard的镜面高光颜色三、具体的BRDF计算对于BRDF的具体计算&#xff0c;在下篇文章中&#xff0c;继续解析 四、最终代码.cginc文件Shader文件 前言 在上篇文章中&#xff0c;我们解析…

Lasso回归

【小白从小学Python、C、Java】 【计算机等考500强证书考研】 【Python-数据分析】 Lasso回归 以下代码的说法中正确的是? import numpy as np import matplotlib.pyplot as plt x np.array([[1],[2],[3],[4]]) y np.array([1,3,6,10]) from sklearn.linear_model impor…

电脑热点无法使用,分配IP地址失败

电脑热点无法使用&#xff0c;分配IP地址失败 不知道从什么时候起电脑开热点就无法连接上了&#xff0c;手机提示无法分配IP地址&#xff0c;电脑正常显示。 设置共享网络连接时提示以下内容。 无法启用internet连接共享,为LAN连接配置的IP地址需要使用自动IP寻址 查阅相关资…

java编程:给定⼀组正整数数组M,找出M数组中N项和为给定数S。如果有多对N项数字的和都等于 S,则输出N个数的乘积最⼩的哪⼀项,没有则返回空

题目&#xff1a; 编程题&#xff1a;给定⼀组正整数数组M&#xff0c;找出M数组中N项和为给定数S。如果有多对N项数字的和都等于 S&#xff0c;则输出N个数的乘积最⼩的哪⼀项&#xff0c;没有则返回空&#xff1b; 程序如下&#xff1a; 测试主程序&#xff1a; 先看下测试示…

【操作宝典】SQL巨擘:掌握SQL Server Management的终极秘籍!

目录 ⛳️【SQL Server Management】 ⛳️1. 启动登录 ⛳️2. 忘记密码 ⛳️3. 操作数据库和表 3.1 新建数据库text 3.2 新建表 3.3 编辑表 3.4 编写脚本 ⛳️【SQL Server Management】 ⛳️1. 启动登录 需要开启服务 ⛳️2. 忘记密码 登录windows--> 安全性 -->…

Windows11安装后跳过联网登录

Windows11安装后跳过联网登录 实验设备&#xff1a; VMware17Pro虚拟机中使用Windows11镜像安装Windows11操作系统&#xff0c;并且在虚拟机中测试跳过联网登录。 步骤 说明&#xff1a;物理卸载网卡&#xff08;在虚拟机上禁用网卡&#xff09;没用 思路&#xff1a; sh…