我们知道,线程的创建和销毁都需要映射到操作系统,因此其代价是比较高昂的。出于避免频繁创建、销毁线程以及方便线程管理的需要,线程池应运而生。
有句话叫做艺术来源于生活,编程语言也是如此,很多设计思想能映射到日常生活中,比如面向对象思想、封装、继承,等等。今天我们要说的线程池,它同样可以在现实世界找到对应的实体——工厂。
先假想一个工厂的生产流程:
工厂中有固定的一批工人,称为正式工人,工厂接收的订单由这些工人去完成。当订单增加,正式工人已经忙不过来了,工厂会将生产原料暂时堆积在仓库中,等有空闲的工人时再处理(因为工人空闲了也不会主动处理仓库中的生产任务,所以需要调度员实时调度)。仓库堆积满了后,订单还在增加怎么办?
工厂只能临时扩招一批工人来应对生产高峰,而这批工人高峰结束后是要清退的,所以称为临时工。当时临时工也以招满后(受限于工位限制,临时工数量有上限),后面的订单只能忍痛拒绝了。
我们做如下一番映射:
getTask()是一个方法,将任务队列中的任务调度给空闲线程,在解读线程池有详细介绍
映射后,形成线程池流程图如下,两者是不是有异曲同工之妙?
这样,线程池的工作原理或者说流程就很好理解了,提炼成一个简图:
那么接下来,问题来了,线程池是具体如何实现这套工作机制的呢?从Java线程池Executor框架体系可以看出:线程池的真正实现类是ThreadPoolExecutor,因此我们接下来重点研究这个类。
研究一个类,先从它的构造方法开始。ThreadPoolExecutor提供了4个有参构造方法:
解释一下构造方法中涉及到的参数:
放到一起再看一下:
使用ThreadPoolExecutor需要指定一个实现了BlockingQueue接口的任务等待队列。在ThreadPoolExecutor线程池的API文档中,一共推荐了三种等待队列,它们是:SynchronousQueue、LinkedBlockingQueue和ArrayBlockingQueue;
另外,Java还提供了另外4种队列:
线程池有一个重要的机制:拒绝策略。当线程池workQueue已满且无法再创建新线程池时,就要拒绝后续任务了。拒绝策略需要实现接口,不过Executors框架已经为我们实现了4种拒绝策略:
线程工厂指定创建线程的方式,这个参数不是必选项,Executors类已经为我们非常贴心地提供了一个默认的线程工厂:
线程池有5种状态:
runState表示当前线程池的状态,它是一个 volatile 变量用来保证线程之间的可见性。
下面的几个static final变量表示runState可能的几个取值,有以下几个状态:
1、线程初始化
默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程。
在实际中如果需要线程池创建之后立即创建线程,可以通过以下两个方法办到:
2、线程池关闭
ThreadPoolExecutor提供了两个方法,用于线程池的关闭:
3、线程池容量调整
ThreadPoolExecutor提供了动态调整线程池容量大小的方法:
当上述参数从小变大时,进行线程赋值,还可能立即创建新的线程来执行任务。
ThreadPoolExecutor
通过构造方法使用是线程池最直接的使用方式,下面看一个实例:
运行结果:
另外,Executors封装好了4种常见的功能线程池(还是那么地贴心):
1、FixedThreadPool
固定容量线程池。其特点是最大线程数就是核心线程数,意味着线程池只能创建核心线程,keepAliveTime为0,即线程执行完任务立即回收。任务队列未指定容量,代表使用默认值。适用于需要控制并发线程的场景。
使用示例:
2、 SingleThreadExecutor
单线程线程池。特点是线程池中只有一个线程(核心线程),线程执行完任务立即回收,使用有界阻塞队列(容量未指定,使用默认值)
使用示例:
3、 ScheduledThreadPool
定时线程池。指定核心线程数量,普通线程数量无限,线程执行完任务立即回收,任务队列为延时阻塞队列。这是一个比较特别的线程池,适用于执行定时或周期性的任务。
使用示例:
4、CachedThreadPool
缓存线程池。没有核心线程,普通线程数量为(可以理解为无限),线程闲置60s后回收,任务队列使用这种无容量的同步队列。适用于任务量大但耗时低的场景。
使用示例:
解读线程池
OK,相信前面内容阅读起来还算轻松愉悦吧,那么从这里开始就进入深水区了,如果后面内容能吃透,那么线程池知识就真的被你掌握了。
我们知道,向线程池提交任务是用的方法,但在其内部,线程任务的处理其实是相当复杂的,涉及到、、三个类的6个方法:
在类中,任务提交方法的入口是方法(方法也是调用了),该方法其实只在尝试做一件事:经过各种校验之后,调用 方法为线程池创建一个线程并执行任务,与之相对应,的结果有两个:
参数说明:
执行流程:
1、通过 得到线程池的当前线程数,如果线程数小于corePoolSize,则调用 方法创建新的线程执行任务,否则执行步骤2;
2、步骤1失败,说明已经无法再创建新线程,那么考虑将任务放入阻塞队列,等待执行完任务的线程来处理。基于此,判断线程池是否处于Running状态(只有Running状态的线程池可以接受新任务),如果任务添加到任务队列成功则进入步骤3,失败则进入步骤4;
3、来到这一步需要说明任务已经加入任务队列,这时要二次校验线程池的状态,会有以下情形:
4、将线程池扩容至并调用 方法创建新的线程执行任务,失败则拒绝本次任务。
流程图:
源码详读:
方法,顾名思义,向线程池添加一个带有任务的工作线程。
参数说明:
执行流程:
1、外层循环判断线程池的状态是否可以新增工作线程。这层校验基于下面两个原则:
2、内层循环向线程池添加工作线程并返回是否添加成功的结果。
3、核心线程数量+1成功的后续操作:添加到工作线程集合,并启动工作线程
流程图:
源码详读:
Worker类
Worker类是内部类,既实现了Runnable,又继承了(以下简称AQS),所以其既是一个可执行的任务,又可以达到锁的效果。
Worker类主要维护正在运行任务的线程的中断控制状态,以及其他次要的记录。这个类适时地继承了类,以简化获取和释放锁(该锁作用于每个任务执行代码)的过程。这样可以防止去中断正在运行中的任务,只会中断在等待从任务队列中获取任务的线程。
我们实现了一个简单的不可重入互斥锁,而不是使用可重入锁,因为我们不希望工作任务在调用之类的池控制方法时能够重新获取锁。另外,为了在线程真正开始运行任务之前禁止中断,我们将锁状态初始化为负值,并在启动时清除它(在runWorker中)。
可以说,是线程池中真正处理任务的方法,前面的和 都是在为该方法做准备和铺垫。
参数说明:
执行流程:
1、判断当前任务或者从任务队列中获取的任务是否不为空,都为空则进入步骤2,否则进入步骤3
2、任务为空,则将completedAbruptly置为false(即线程不是突然终止),并执行方法进入线程退出程序
3、任务不为空,则进入循环,并加锁
4、判断是否为线程添加中断标识,以下两个条件满足其一则添加中断标识:
5、执行前置方法 (该方法为空方法,由子类实现)后执行方法执行任务(执行不成功抛出相应异常)
6、执行后置方法 (该方法为空方法,由子类实现)后将线程池已完成的任务数+1,并释放锁。
7、再次进行循环条件判断。
流程图:
源码详读:
由函数调用关系图可知,在类的实现中,方法是为方法服务的,它的作用就是在任务队列(workQueue)中获取 task(Runnable)。
参数说明:无参数
执行流程:
流程图:
源码详读:
processWorkerExit(Worker w, boolean completedAbruptly)执行线程退出的方法
参数说明:
执行流程:
1、如果 completedAbruptly 为 true,即工作线程因为异常突然死亡,则执行工作线程-1操作。
2、主线程获取锁后,线程池已经完成的任务数追加 w(当前工作线程) 完成的任务数,并从worker的set集合中移除当前worker。
3、根据线程池状态进行判断是否执行tryTerminate()结束线程池。
4、是否需要增加工作线程,如果线程池还没有完全终止,仍需要保持一定数量的线程。
源码详读:
好啦,以上就是Java线程池的全部内容啦,坚持读完的伙伴儿们你们收获如何?觉得有帮助的就顺手点个赞吧。
版权声明:
本文来源网络,所有图片文章版权属于原作者,如有侵权,联系删除。
本文网址:https://www.mushiming.com/mjsbk/14975.html