🌄写在前面
什么是线程池?
线程池是一种基于池化思想的线程管理工具,常常用在多线程服务器上。如果不使用线程管理工具,可能会产生下面的问题:
- 线程过多会带来额外的开销,其中包括创建销毁线程的开销、切换线程上下文的开销等等,降低了计算机的整体性能
而使用线程池维护多个线程,等待监督管理者分配可并发执行的任务。一方面避免了处理任务时创建销毁线程开销的代价,另一方面避免了线程数量膨胀导致的过分调度问题,保证了对内核的充分利用。总的来说使用线程池的好处:
- 降低资源开销:每次任务到达时不需要重新创建和销毁
- 提高可管理性:统一对线程进行调优、监控
- 提高任务响应速度:不需要等待线程创建
线程池解决了什么问题?
线程池解决的核心问题就是资源管理问题。在并发环境下,系统不能够确定在任意时刻中,有多少任务需要执行,有多少资源需要投入。这种不确定性将带来以下若干问题:
- 频繁申请/销毁资源和调度资源,将带来额外的消耗,可能会非常巨大。
- 对资源无限申请缺少抑制手段,易引发系统资源耗尽的风险。
- 系统无法合理管理内部的资源分布,会降低系统的稳定性。
线程池设计思路
总体设计
| 此次分析是以JDK1.8
的ThreadPoolExecutor
为准。该类是整个线程池实现的核心,我们先根据右边的ThreadPoolExecutor
的UML类图,来了解一下它的继承关系。
-Executor
提供任务执行的功能,这部分专门负责任务的执行,让后续开发者只需关注任务的编写和提交即可
-ExecutorService
提供拓展了两项功能,一是批量执行任务功能;二是提供了结束线程的能力
-AbstractExecutorService
是一个抽象类,将上述两个接口的能力进行了整合,保证下层只需要关注一个任务执行方法
-ThreadPoolExecutor
是最终的实现类,它维护着整个池子的状态,同时管理线程和任务,使两者相互配合共同完成任务的执行
| | | —- | —- | | |
运行机制
线程池运行机制少不了两个部分:
- 线程的管理
- 任务的处理
在总体设计里,我们可以看到 Executor
、 ExecutorService
、 AbstractExecutorService
一直在强化任务的执行;那么线程管理的重任自然就落在了 ThreadPoolExecutor
,所以这部分我将分为两个部分讲:
ThreadPoolExecutor
的管理机制AbstractExecutorService
的执行原理
ThreadPoolExecutor 机制概览
FROM 《Java线程池实现原理及其在美团业务中的实践》ThreadPoolExecutor
的整个机制有点类似生产消费模型,外部充当生产者,内部的线程池充当消费者,每当外部有任务提交进来后,ThreadPoolExecutor
会对其进行任务分配:从池子里取一个线程,让该线程执行任务后,执行完毕后将线程归还给池子。在 ThreadPoolExecutor
运行中,任务的分配方式可以分为三种情况:
- 如果线程足够,可以直接执行
- 如果线程不足,则将消息放入任务队列,线程池从队列中获取任务进行执行
- 如果任务数量超出任务队列,则任务拒绝
线程池运行状态
ThreadPoolExecutor
在内部使用一个32位整型变量来维护 运行状态(RunState) 和 线程数量(ThreadCount) 两个值。具体是保存在下面这个变量上:
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
通过这个变量初始值可以看出,线程池新建时处于 **RUNNING**
状态且线程数量为 0。这两个状态通过 ctlOf()
静态方法进行 或运算 得出:
private static int ctlOf(int rs, int wc) { return rs | wc; }
它总共有五种状态:
运行状态 | 描述 |
---|---|
RUNNING | 能接受新的任务,也能处理已经入队了的任务 |
SHUTDOWN | 不接受新的任务,但是还能处理已经入队了的任务 |
STOP | 不接受新的任务,也不处理已经入队列的任务,会中断所有正在处理中的任务 |
TIDYING | 所有的任务都已中止, workCount (工作线程数量)为0 |
TERMINATED | terminated() 方法执行完成后进入该状态 |
其状态转换图如下所示:
FROM 《Java线程池实现原理及其在美团业务中的实践》
关于线程池状态的具体实现可以看《🧺J.U.C - 线程池实现与设计之实现篇》
任务调度
任务调度是所有任务提交的入口。当用户提交了一个任务后,线程池会根据当前线程池的状态来决定如何执行这个任务,检查状态的依据主要由以下三个方面决定:
- 运行状态
- 运行线程数量
- 运行策略
状态检查流程如下所示:
点击查看【processon】
- 任务提交进来时,首先检查线程池的状态,仅为
RUNNING
才继续向后流转;否则,直接拒绝任务提交 - 当 当前运行线程数 小于 核心数
**CoreSize**
时,会新建一个工作线程用于执行新任务;反之继续向下流转 - 当 阻塞队列未满 时,则将任务提交到阻塞队列中等待;反之继续向下流转
- 当 线程数小于最大线程数 时,则继续新建工作线程用于执行新任务;反之拒绝任务的提交