01、ThreadPoolExecutor 线程池源码完整剖析 ------ 线程池工作流程、参数解析、简单创建线程池及使用演示

2023-12-22 16:08:42

线程池源码剖析


## 1、线程池介绍

什么是线程?

线程(Thread)是计算机中能够执行独立任务的最小单元。线程是进程内的一个执行单位,一个进程可以包含多个线程


什么是多线程?

多线程是指在一个程序中同时执行多个线程的并发编程模型。在多线程模型中,程序被设计为可以同时执行多个任务,每个任务运行在独立的线程中


什么是线程池 ?

线程池其实就是一种多线程处理形式,处理过程中可以将任务添加到队列中,然后在创建线程后自动启动这些任务。这里的任务就是实现了Runnable 或 Callable 接口的实例对象;


为什么需要用到线程池 ?

减少线程的创建。

使用线程池最大的原因就是可以根据系统的需求和硬件环境灵活的控制线程的数量,且可以对所有线程进行统一的管理和控制,从而提高系统的运行效率,降低系统运行压力;


使用线程池有哪些优势 ?

1、线程和任务分离,提升线程重用性;

2、控制线程并发数量,降低服务器压力,统一管理所有线程;

3、提升系统响应速度,
假如创建线程用的时间为T1,执行任务用的时间为T2,销毁线程用的时间为T3,那么使用线程池就免去了 T1 和 T3 的时间;


线程的重用性的解释:
如果我们创建一个线程,然后给它一个任务,那么这个线程执行完之后,就会销毁掉,因为无法让这个线程再执行其他任务,因为这个线程跟这个任务是绑定的,是耦合的,只能做这个任务。
如果线程要重复利用,那么就需要用到线程池,我们直接把任务给线程池,具体由哪个线程来执行,是线程池内部控制的。
在这里插入图片描述


线程的应用场景有哪些 ?

1、并发处理:线程可以实现程序的并发执行,提高程序的运行效率。在服务器端开发中,可以使用多线程来处理多个客户端的请求,提高服务器的并发处理能力
2、云盘文件上传和下载,可以开多线程。
3、多线程批量发送短信邮件
4、框架内部很多都使用多线程提高效率(比如 RocketMQ的多个消息队列)


文件多线程下载解释:
在这里插入图片描述


多线程批量发送短信邮件解释:
比如过节,某公司给众多用户发送节假日的短信祝福,如果是单线程,用户量又大,结果单线程的情况下,短信发到第二天才发完,这就会搞出乌龙。用多线程批量发送能提高效率。


2、线程池工作流程


ThreadPoolExecutor参数详解

ThreadPoolExecutor 是 Java 提供的一个线程池实现类,用于管理和调度线程的执行。

在这里插入图片描述

1、核心线程数(corePoolSize)

表示线程池中的核心线程的数量,也可以称为可闲置的线程数量,
默认情况下,这个线程是不会被回收的,但是也可以通过设置进行回收。

注意点:设置2个核心线程,并不是说先创建出来的那2个线程就是核心线程。

假如有很多任务提交到线程池,线程池创建出线程1和线程2来执行任务,后面阻塞队列满了,根据需要又创建了线程3和线程4,后面当把阻塞队列的任务都执行完了,剩下自身线程的任务在执行,如果线程2和线程3先执行完了,在等待60秒都没有新的任务过来,那么线程2和线程3就会被回收销毁,剩下线程1和线程4。
并不是先创建的线程就是核心线程,而是最后做完任务的那两个线程,因为剩最后两个线程不会被回收,所以也可以当成是核心线程来说。


2、最大线程数 (maximumPoolSize)

当任务比较繁忙,核心线程数在处理任务,其他任务会先被放入阻塞队列中,当阻塞队列任务放满之后,这时候会创建新的线程,创建的线程最多只能达到设置的最大线程数。


3、非核心线程存活时间 (keepAliveTime)

在任务繁忙时,会创建非核心的线程,创建的数量不会超过我们设置的最大线程数。然后这些线程在执行完阻塞队列中的任务后,如果在我们设置的等待时间内,没有新的任务需要处理,那么这些非核心线程就会被销毁掉。
非核心线程没事干,最多能等待的时间,就是这个非核心线程存活时间。


4、缓存任务的阻塞队列 (BlockingQueue)

当核心线程都在处理任务的情况下,新加入的任务会被放到这个阻塞队列中,等待被线程获取。

这个队列的特点是先进先出。


5、创建线程的工厂 (ThreadFactory)

既然是线程池,那自然少不了线程,线程该如何来创建呢?这个任务就交给了线程工厂 ThreadFactory 来完成。

就是线程池内部使用的线程,就是由这个工厂创建出来的。


6、拒绝策略 (RejectedExecutionHandler)

当阻塞队列存放的任务满了,线程数也达到了最大线程数,对新加入的任务执行拒绝策略


策略1:AbortPolicy(默认策略)

丢弃任务 并抛出 RejectedExecutionException 异常。

在这里插入图片描述


策略2:DiscardPolicy
也是丢弃任务,但是不抛出异常。


策略3:DiscardOldestPolicy
丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)


策略4:CallerRunsPolicy
由调用线程处理该任务


简单创建和使用线程池

创建一个线程池,把任务丢给线程池,线程池就会开线程去执行任务。

如图:创建一个线程池,然后执行10个任务。

执行逻辑是:
当前有2个核心线程,这两个核心线程在处理任务时,
如果还有任务过来,这时线程池是不会创建线程的,是先把任务丢到这个缓存任务的阻塞队列里面去等待执行的。
如果这个阻塞队列满了,线程池才会继续创建线程来执行任务。
如果开的线程达到设置的最大线程数了,这时还有任务过来,线程池处理不过来,就会报错:RejectedExecutionException 拒绝执行的异常


演示:前4个参数

线程池的 execute()方法 用于向线程池提交一个任务,该任务将由线程池中的线程来执行。
在execute()方法中,通常会传入一个Runnable对象,该对象表示要执行的任务逻辑。

这个线程池使用到的参数: 核心线程数(corePoolSize)、最大线程数 (maximumPoolSize)、非核心线程存活时间 (keepAliveTime)、缓存任务的阻塞队列 (BlockingQueue)
在这里插入图片描述

缓存任务的阻塞队列,容量为10,最大线程数是4,10+4=14;
加起来这个线程池能接受的最大的执行任务数量就是 14,

如果执行的任务是15次,**超过线程池的最大容量,**就会报错,触发拒绝策略
在这里插入图片描述

如果执行14次任务,在最大限度内,就不会报错。
在这里插入图片描述


演示:创建线程的工厂 (ThreadFactory) 这个参数

没用这个参数的时候,创建的线程是默认的。

用这个参数来创建线程,可以自己定义线程的名字或其他的东西等。
在这里插入图片描述


线程池代码分析图

按这个代码来画图分析
在这里插入图片描述


步骤解析:

1、我们设置的核心线程是2个,这个时候有任务提交到线程池,那么线程池就会创建线程来执行任务。

注意:一开始,没有任务提交到线程池的时候,这个线程池是没有任何线程的。直到提交任务到线程池,线程池才会创建线程来执行任务。

提交第1个任务,那么就创建一个线程1来执行,这时提交第2个任务,如果线程1还没执行完,那么就创建线程2来执行任务。
如果提交第2个任务的时候,线程1已经执行完了,那么该任务就交给线程1 来执行,就不会再创建线程2出来。
在这里插入图片描述

2、如果线程1和线程2还在执行任务,这个时候又来第3个任务提交到线程池。如图

线程池就会把任务先丢到阻塞队列里面去,等这两个核心线程有哪个先执行完自己的任务后,再去阻塞队列里面获取任务来执行。

注意:阻塞队列的特点是先进先出。
在这里插入图片描述


3、如果这个时候,又有11个任务提交到线程池。那么此时一共有14个任务提交到线程池了,如图

如果线程1和线程2 还在执行任务,然后阻塞队列又存满了,那么线程池就会继续创建线程来执行任务,但是创建的线程是不会超过设置的最大线程数的。

在这里插入图片描述

4、如果这个时候,再提交第15个任务到线程池,但是阻塞队列已经存满了,最大线程数4个,也都在执行任务,没有空闲的线程了,达到线程池的最大性能数了,那么就会触发任务的拒绝策略。

在这里插入图片描述


5、如果任务都执行完了,非核心线程在60秒内没有拿到其他任务,那么就会被销毁,就只剩下2个核心线程保留下来。
核心线程是一直存在的,不会被销毁的。
核心线程数、最大线程数、非核心线程数空闲存活时间,都是由我们自己设置的。

注意点:设置2个核心线程,并不是说先创建出来的那2个线程就是核心线程。

假如有很多任务提交到线程池,线程池创建出线程1和线程2来执行任务,后面阻塞队列满了,根据需要又创建了线程3和线程4,后面当把阻塞队列的任务都执行完了,剩下自身线程的任务在执行,如果线程2和线程3先执行完了,在等待60秒都没有新的任务过来,那么线程2和线程3就会被回收销毁,剩下线程1和线程4。
并不是先创建的线程就是核心线程,而是最后做完任务的那两个线程,因为剩最后两个线程不会被回收,所以也可以当成是核心线程来说。
在这里插入图片描述


演示拒绝策略


策略1:AbortPolicy(默认策略)

丢弃任务 并抛出 RejectedExecutionException 异常。

在这里插入图片描述


策略2:DiscardPolicy
也是丢弃任务,但是不抛出异常。
如果,没看到-----执行第【14】个任务,因为这是最后一个任务,直接被丢弃了
在这里插入图片描述


策略3:DiscardOldestPolicy
丢弃阻塞队列最前面的任务,然后重新尝试执行任务(重复此过程)
在这里插入图片描述


策略4:CallerRunsPolicy
由调用线程处理该任务

因为是main这个线程调用了线程池,所以这第14个任务,就交由main线程处理;在main线程空闲的时候,就会处理这个任务。
在这里插入图片描述


演示关闭线程池

在这里插入图片描述


shutdown()

关闭线程池的话,优先考虑这个方法。

线程池的 shutdown()方法 是用于关闭线程池的操作。
关闭线程池:调用这个方法之后,后续添加的任务是不会再被执行了,但是已经加入的任务会继续执行完

如图:在 i==10 之前,已经往线程池加入9个任务了。
调用shutdown()方法后,线程池将不再接受新的任务,并且会等待所有已提交的任务执行完毕。一旦所有任务完成,线程池中的线程将被终止,线程池也将被关闭。
在这里插入图片描述


shutdownNow()

关闭线程池:调用这个方法之后,后续添加的任务是不会再被执行了,但是已经加入的任务,如果还没有开始执行,就不会再去执行了。
停止所有正在执行的任务,暂停处理正在等待的任务,并返回等待执行的任务列表。

shutdownNow() 方法用于立即关闭线程池。与shutdown()方法不同的是,shutdownNow()方法会尝试停止所有正在执行的任务,并且不会等待尚未开始的任务执行完成。
shutdownNow() 方法会尝试通过中断正在执行的线程来停止任务的执行。如果一个线程已经被中断,则会抛出InterruptedException异常。如果线程池中的某个任务无法被中断,则该任务将继续运行直到完成。
在这里插入图片描述

关闭线程池的结果。
已经加入线程池的 2 ~ 9 个任务,可能正在执行,或等待执行,
正在执行的任务如果能被终止,那么就会被终止掉,然后抛出InterruptedException异常。
还没有执行但已经加入线程池的任务,也不会执行。
在这里插入图片描述


线程池的参数设计分析


核心线程数(corePoolSize)

核心线程数的设计需要依据任务的处理时间和每秒产生的任务数量来确定。
例如:一个线程执行一个任务需要0.1秒,1秒就执行10个任务;系统80%的时间每秒都会产生100个任务,那么要想在1秒内处理完这100个任务,就需要10个线程。

此时我们就可以设计核心线程数为10;

当然实际情况不可能这么平均,所以我们一般按照二八原则设计即可,即按照80%的情况设计核心线程数,剩下的20%可以利用最大线程数处理;


任务队列长度(workQueue)

任务队列长度,也就是设计 阻塞队列 能缓存多少个任务。

任务队列长度一般设计为:

核心线程数 / 单个任务执行时间 *2 即可;

例如上面的场景中,核心线程数设计为10,单个任务执行时间为0.1秒,则队列长度可以设计为200 ;


最大线程数(maximumPoolSize)

最大线程数的设计除了需要参照核心线程数的条件外,还需要参照系统每秒产生的最大任务数决定;

例如:上述环境中,如果系统每秒最大产生的任务是1000个,那么:

最大线程数 = ( 最大任务数 - 任务队列长度 ) * 单个任务执行时间;

即: 最大线程数 = (1000 - 200 ) * 0.1 = 80个;


最大空闲时间(keepAliveTime)

这个参数的设计完全参考系统运行环境和硬件压力设定,没有固定的参考值,用户可以根据经验和系统产生任务的时间间隔合理设置一个值即可。


总流程分析图

在这里插入图片描述


文章来源:https://blog.csdn.net/weixin_44411039/article/details/135152780
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。