10000字吐血总结+24张图带你彻底弄懂线程池

news2024/9/21 16:21:44

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

一、什么是线程池

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

使用线程池的好处

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统 的稳定性,使用线程池可以进行统一的分配,调优和监控。

二、线程池的构造

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

ThreadPoolExecutor的构造方法

img

  • corePoolSize:线程池中用来工作的核心的线程数量。
  • maximumPoolSize:最大线程数,线程池允许创建的最大线程数。
  • keepAliveTime:超出 corePoolSize 后创建的线程存活时间或者是所有线程最大存活时间,取决于配置。
  • unit:keepAliveTime 的时间单位。
  • workQueue:任务队列,是一个阻塞队列,当线程数已达到核心线程数,会将任务存储在阻塞队列中。
  • threadFactory :线程池内部创建线程所用的工厂。
  • handler:拒绝策略;当队列已满并且线程数量达到最大线程数量时,会调用该方法处理该任务。

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

三、线程池的运行原理

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

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

img

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

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

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

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

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

img

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

img

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

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

img

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

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

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

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

img

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

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

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

img

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

  • AbortPolicy:丢弃任务,抛出运行时异常
  • CallerRunsPolicy:由提交任务的线程来执行任务
  • DiscardPolicy:丢弃这个任务,但是不抛异常
  • DiscardOldestPolicy:从队列中剔除最先进入队列的任务,然后再次提交任务

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

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

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

img

  • workerCountOf©<corePoolSize:这行代码就是判断是否小于核心线程数,是的话就通过addWorker方法,addWorker就是添加线程来执行任务。
  • workQueue.offer(command):这行代码就表示尝试往阻塞队列中添加任务
  • 添加失败之后就会再次调用addWorker方法尝试添加非核心线程来执行任务
  • 如果还是添加非核心线程失败了,那么就会调用reject(command)来拒绝这个任务。

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

img

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

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

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

img

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

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

img

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

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

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

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

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

img

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方法以及线程超时退出的机制如图所示

img

六、线程池的5种状态

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

img

  • RUNNING:线程池创建时就是这个状态,能够接收新任务,以及对已添加的任务进行处理。
  • SHUTDOWN:调用shutdown方法线程池就会转换成SHUTDOWN状态,此时线程池不再接收新任务,但能继续处理已添加的任务到队列中任务。
  • STOP:调用shutdownNow方法线程池就会转换成STOP状态,不接收新任务,也不能继续处理已添加的任务到队列中任务,并且会尝试中断正在处理的任务的线程。
  • TIDYING:
    SHUTDOWN 状态下,任务数为 0, 其他所有任务已终止,线程池会变为 TIDYING 状态。
    线程池在 SHUTDOWN 状态,任务队列为空且执行中任务为空,线程池会变为 TIDYING 状态。
    线程池在 STOP 状态,线程池中执行中任务为空时,线程池会变为 TIDYING 状态。
  • TERMINATED:线程池彻底终止。线程池在 TIDYING 状态执行完 terminated() 方法就会转变为 TERMINATED 状态。

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

privatefinalAtomicIntegerctl=newAtomicInteger(ctlOf(RUNNING, 0));

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

img

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

七、线程池的关闭

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

shutdown方法

img

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

shutdownNow方法

img

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

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

八、线程池的监控

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

  • getCompletedTaskCount:已经执行完成的任务数量
  • getLargestPoolSize:线程池里曾经创建过的最大的线程数量。这个主要是用来判断线程是否满过。
  • getActiveCount:获取正在执行任务的线程数据
  • getPoolSize:获取当前线程池中线程数量的大小

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

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

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

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

img

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

img

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

img

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

img

虽然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。

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

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

相关文章

MVC|JAVA|SSM框架计算机硬件评测交流平台的开发和实现

收藏点赞不迷路 关注作者有好处 文末获取源码 项目编号&#xff1a;BS-PT-070 一&#xff0c;项目简介 计算机硬件在社会上有很多广泛的发烧友&#xff0c;他们急需一个发布专业硬件测评数据的平台并进行交流互动的社区。本次开发实现的计算机硬件交流平台就是作为一个专业的…

Android序列化之Parcel源码分析(2)

文章目录1.Parcel.java2.Parcelable和Parcel的关系3.Parcel写入数据源码分析3.1.java层Parcel创建3.2.native层Parcel创建3.3写入IBinder接口标识符3.4写入String数据4.Parcel读取数据源码分析4.1获取IBinder接口标识符4.2读取String数据1.Parcel.java Android可以通过Parcel进…

【OpenCV学习】第15课:处理卷积边缘问题

仅自学做笔记用,后续有错误会更改 &#xff08;卷积的概念可以看看第14课&#xff09; 理论 卷积边缘问题&#xff1a;从下图最右方的结果可以看出&#xff0c;卷积操作之后&#xff0c; 剩余的绿色像素部分&#xff0c; 我们是没有处理到的 那么如何处理这个问题呢&#xf…

论文3:查找文献在指定期刊的引用格式

文章目录说明&#xff1a;1.谷歌学术搜索&#xff08;可以用一些国内的镜像&#xff09;&#xff0c;并点击被引用次数2.勾选在引用文章中搜索&#xff0c;并在搜索框搜索指定期刊的关键词3.这里指定期刊是RAL即IEEE Robotics and Automation Letters4.任意点开上图中的一篇文章…

支付宝当面付网站对接支付教程

有很多人会开支付宝当面付但是不会配置它老会出现一下情况 第二种情况如下&#xff1a; 如果遇到以上情况可以按照我的步骤就可以解决 详细步骤&#xff1a; 一、应用APPID获取方法 1.打开网站&#xff1a;https://openhome.alipay.com/platform/developerIndex.htm&#x…

Canal配置多个实例以及将Mysql指定表的binlog导入指定的Kafka的Topic

Canal配置多个实例以及将Mysql指定表的binlog导入指定的Kafka的Topic 进入Canal的conf目录 复制模板配置文件 cp -r example/ Ordercp -r example/ Orderdetail修改canal.propertieswenjain vim canal.properties修改内容如下&#xff0c;指定输出模式为kafka canal.serverM…

【元胞自动机】心房颤动/扑动模型研究(Matlab代码实现)

&#x1f468;‍&#x1f393;个人主页&#xff1a;研学社的博客 &#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜…

(附源码)ssm学校疫情服务平台 毕业设计 291202

ssm学校疫情服务平台 摘 要 信息化社会内需要与之针对性的信息获取途径&#xff0c;但是途径的扩展基本上为人们所努力的方向&#xff0c;由于站在的角度存在偏差&#xff0c;人们经常能够获得不同类型信息&#xff0c;这也是技术最为难以攻克的课题。针对学校疫情服务平台等问…

【数电实验】触发器及其应用

实验三 触发器及其应用 一 实验目的 1 了解触发器的触发方式&#xff08;上升沿触发、下降沿出发&#xff09;及其触发特点&#xff1b; 2 测试常用触发器的逻辑功能&#xff1b; 3 掌握用触发器设计同步时序逻辑电路的方法。 二 实验内容 1 测试双D触发器74HC74的逻辑功能…

手工编译konsole备忘

背景 系统自带的终端弱爆了&#xff0c;本来想编译深度终端的&#xff0c;但DTK风格的程序在非DDE桌面&#xff08;应该是dde_kwin这个窗管的问题&#xff09;巨难看&#xff0c;无意中添加了Konsole&#xff0c;发现已经有我需要使用的右键打开当前目录文件管理器的功能。 …

Go context.Context的学习

一、前言 Golang context是Golang应用开发常用的并发控制技术&#xff0c;它与WaitGroup最大的不同点是context对于派生goroutine有更强的控制力&#xff0c;它可以控制多级的goroutine。 context翻译成中文是”上下文”&#xff0c;即它可以控制一组呈树状结构的goroutine&a…

java计算机毕业设计ssm疫情期间校园车辆入校预约管理服务系统1171a(附源码、数据库)

java计算机毕业设计ssm疫情期间校园车辆入校预约管理服务系统1171a&#xff08;附源码、数据库&#xff09; 项目运行 环境配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe…

没有二十年功力,写不出 Thread.sleep(0) 这一行“看似无用”的代码

这篇文章要从一个奇怪的注释说起&#xff0c;就是下面这张图&#xff1a; 我们可以不用管具体的代码逻辑&#xff0c;只是单单看这个 for 循环。 在循环里面&#xff0c;专门有个变量 j&#xff0c;来记录当前循环次数。 第一次循环以及往后每 1000 次循环之后&#xff0c;进…

ssm+vue基本微信小程序的校园二手商城系统 计算机毕业设计

在当今社会的高速发展过程中&#xff0c;产生的劳动力越来越大&#xff0c;提高人们的生活水平和质量&#xff0c;尤其计算机科技的进步&#xff0c;数据和信息以人兴化为本的目的&#xff0c;给人们提供优质的服务&#xff0c;其中网上购买二手商品尤其突出&#xff0c;使我们…

211大数据专业大四学生,放弃字节转正,选择老家大型国企,听听他怎么说?...

点击上方 "大数据肌肉猿"关注, 星标一起成长点击下方链接&#xff0c;进入高质量学习交流群今日更新| 1052个转型案例分享-大数据交流群分享学习群一位大数据专业同学的秋招学习和求职经历&#xff0c;他是211大四学生&#xff0c;年初才开始学习&#xff0c;但还好赶…

181.基于Django的云文件存储使用方式——七牛云存储

1.文件云存储 1.1 概述 在Django项目中&#xff0c;用户上传的文件以及项目中使用的静态文件&#xff0c;默认读书存储本地&#xff0c;保存在服务器中&#xff0c;但是&#xff0c;其实我们也可以将他们保存在云存储中&#xff0c;譬如七牛云存储、阿里云存储、亚马逊云存储…

【网络安全】提防黑客来“敲门”

前言 互联网在给我们带来便捷高效的同时&#xff0c;也给一些不法分子提供了可乘之机。网络诈骗、窃取个人信息等花样层出不穷&#xff0c;骚扰电话、垃圾短信扰乱着我们的正常生活&#xff0c;使网络空间抹上一笔灰色。网络安全与每个人都息息相关&#xff0c;所以我们必须理…

Python测试进阶(三)

文章目录性能测试JMeter测试计划模拟并发结果分析分布式性能监控grafanaFluxPrometheus小结性能测试 为什么做性能测试&#xff1f;主要是解决这些问题 什么是性能测试 模拟多个用户的操作&#xff0c;看对服务器性能的影响 指标 TPS&#xff1a;transaction per secondRT&…

基于Kubeadm快速部署一个K8s集群

目录kubeadm概述安装要求准备环境安装kubelet、kubeadm、kubectl使用kubeadm引导集群下载各个机器需要的镜像初始化主节点安装网络组件常用shell命令测试kubernetes集群部署dashboardkubeadm概述 kubeadm是官方社区推出的一个用于快速部署kubernetes集群的工具。 这个工具能通…

索引创建、删除的sql语句

目录 创建索引 使用ALTER TABLE 语句创建索引 使用CREATE TABLE 语句创建索引 删除索引 使用ALTER TABLE 语句删除索引 使用DROP INDEX 语句删除索引 创建索引 1、创建表的同时&#xff0c;指定给某个字段创建索引&#xff08;name&#xff09; create table cat(id …