文章目录
- 一、前言🚀🚀🚀
- 二、序子:☀️☀️☀️
-
-
- [2.1 线程池是什么?为什么要用线程池?](#2.1 线程池是什么?为什么要用线程池?)
-
- 三、线程池核心设计与实现
-
- [3.1 总体设计](#3.1 总体设计)
-
- [① 各层接口以及ThreadPoolExecutor实现类](#① 各层接口以及ThreadPoolExecutor实现类)
- [② ThreadPoolExecutor 如何同时维护线程和执行任务](#② ThreadPoolExecutor 如何同时维护线程和执行任务)
- [3.2 生命周期管理](#3.2 生命周期管理)
-
- [① 线程池运行状态和线程数量](#① 线程池运行状态和线程数量)
-
- 重点:线程池如何维护⾃身状态
- [重点:用一个变量来存储runState跟workerCount 的作用](#重点:用一个变量来存储runState跟workerCount 的作用)
- [② ThreadPoolExecutor 运行的五种状态](#② ThreadPoolExecutor 运行的五种状态)
- [3.3 任务执行机制](#3.3 任务执行机制)
-
- [① 任务调度](#① 任务调度)
- [② 任务缓冲](#② 任务缓冲)
- [③ 任务申请](#③ 任务申请)
- [④ 任务拒绝](#④ 任务拒绝)
- [3.4 Worker 线程管理](#3.4 Worker 线程管理)
-
- [① Worker线程](#① Worker线程)
- 四、线程池在业务中的实践
一、前言🚀🚀🚀

☀️
回报不在行动之后,回报在行动之中。
这个系列是关于后端场景的研究探讨,本期文章参考美团技术团队《Java线程池实现原理及其在美团业务中的实践》技术文章,由此作者深入研究Java线程池的底层实现以及在项目中的常见应用场景。
二、序子:☀️☀️☀️
J.U.C提供的线程池ThreadPoolExecutor类,帮助开发人员管理线程并方便地执行并行任务。了解并合理使用线程池,是一个开发人员必修的基本功。
2.1 线程池是什么?为什么要用线程池?
线程池(Thread Pool)是一种基于池化思想管理线程的工具, 为了最大化收益并最小化风险,而将资源统一在一起管理的一种思想。
线程池解决的核心问题是资源管理问题。在并发环境下,系统不能够确定在任意时刻中,需要投入多少资源以及需要执行多少任务,这种不确定性会导致如下问题:
① 首先频繁申请/销毁/调度资源,线程过多会带来额外的开销 ,其中包括创建销毁线程的开销、调度线程的开销等等,同时也降低了计算机的整体性能。
② 对资源无限申请缺少抑制手段, 易引发系统资源耗尽的风险。
线程池维护多个线程,等待监督管理者分配可并发执行的任务。 这种做法,一方面避免了处理任务时创建销毁线程开销的代价,另一方面避免了线程数量膨胀导致的过分调度问题,保证了对内核的充分利用。
而本文描述线程池是JDK中提供的ThreadPoolExecutor类。
使用线程池带来的好处如下:
- 降低资源消耗: 通过池化技术重复利用已经创建的线程,避免频繁的创建和销毁线程。
- 提供响应速度: 任务到达时,无需创建线程即可直接执行任务。
- 提供线程的可管理性: 线程是稀缺资源,如果无限制创建线程,不仅会消耗系统资源,还会因为线程的分布不合理导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调度和监控。
三、线程池核心设计与实现
3.1 总体设计

Executor 接口是 ThreadPoolExecutor 实现类的顶级接口。
① 各层接口以及ThreadPoolExecutor实现类
part 1:提供了一种思想(作用):将任务提交和任务执行进行解耦。用户(这里指的是调用该接口方)无需关注如何创建线程,如何调度线程来执行任务。
part 2:用户只需提供 Runnable 对象,将任务的执行逻辑提交到执行器(Executor)中,由Executor框架完成线程的调配和任务的执行部分。
ExecutorService 接口增加了一些能力:
⑴ 扩充执行任务的能力,补充可以为一个活一批异步任务生成Future 的方法;
⑵ 提供了管控线程池的方法,比如停止线程池的运行。
AbstractExecutorService 则是上层的抽象类,将执行任务的流程串联了起来,保证下层的实现只需关注一个执行任务的方法即可。
ThreadPoolExecutor 实现最复杂的运行部分,ThreadPoolExecutor 将会一方面维护自身的生命周期,另一方面同时管理线程和任务,使两者良好的结合从而执行并行任务。
② ThreadPoolExecutor 如何同时维护线程和执行任务

这个图画的真好,将线程池的执行逻辑简单清晰的表明了,主要就是三个部分:任务分配、阻塞队列、线程池。
重点:生产者消费者模型
由这张图可以明白,这其实也是一种生产者跟消费者的关系,同时也将线程和任务两者解耦,并不直接关联,从而良好的缓冲任务,复用线程。
线程池的运行主要分成两部分:任务管理(生产者)、线程管理(消费者)。
当任务提交后,线程池会判断该任务后续的流转:
⑴ 直接申请线程执行该任务
⑵ 缓冲到队列中等待线程执行
⑶ 拒绝该任务
线程管理部分是消费者,它们被统⼀维护在线程池内,根据任务请求进⾏线程的分配,当线程
执⾏完任务后则会继续获取新的任务去执⾏,最终当线程获取不到任务的时候,线程就会被回收。

3.2 生命周期管理
① 线程池运行状态和线程数量
重点:线程池如何维护⾃身状态
线程池运行的状态,并不是用户显示设置的,而是伴随着线程池的运行,由内部来维护。线程池内部使用一个变量维护两个值:运行状态(runState) 和 线程数量(workerCount) 。
在具体实现中,线程池将这两个关键参数的维护放在了一起,如下代码所示:

ctl 这个 原子类 AtomicInteger 类型,是对线程池的 运行状态 和 线程池中 有效线程的数量 进行控制的一个字段。
这个字段同事包含两部分信息:
① 线程池的运行状态(runState)高3位保存
② 线程池内有效线程的数量(workerCount)低29位保存
重点:用一个变量来存储runState跟workerCount 的作用
两个变量之间互不干扰,用一个变量去存储两个值,可避免在做相关决策时,出现不一致的情况,不必为了维护两者的一致,而占用锁资源。
Tip:通过阅读线程池源代码也可以发现,经常出现要 同时 判断线程池 运行状态 和 线程数量 的情况。线程池也提供了若干方法去供用户获得线程池当前的运行状态、线程个数。这里都使用的是 位运算 的方式,相比于基本运算,速度也会快很多。
关于内部封装的获取生命周期状态、获取线程池线程数量的计算方法如以下代码所示:
② ThreadPoolExecutor 运行的五种状态
这里也可以对应线程的五种状态(New、Runnable、Running、Blocked、Dead)
咱们先罗列一下 线程的五种状态:

① New (新建状态):线程对象被创建后,就进入新建对象。Thread thread = new Thread();
② Runnable (就绪状态):处于就绪状态,随时可能被CPU调度执行。
线程被创建后,其他线程调用了该线程的 start 方法,从而启动线程。thread.start();
③ Running (运行状态):线程获取CPU权限进行执行,只能从就绪状态进入。
④ Blocked (阻塞状态):线程因为某种原因放弃CPU使用权,暂时停止运行,直到进入就绪状态才有机会转入运行状态。
阻塞的情况分三种:
(01) 等待 阻塞 -- 通过调用线程的 wait() 方法,让线程等待某工作的完成。
(02) 同步 阻塞 -- 线程在获取 synchronized 同步锁 失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
(03) 其他阻塞 -- 通过调用线程的 sleep () 或 join () 或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
⑤ Dead(死亡状态): 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
大概熟悉线程的五种状态后,我们再来看看ThreadPoolExecutor 的五种运行状态:

① RUNNING: 能接受新提交的任务,并且也能处理阻塞队列中的任务。
② SHUTDOWN: 关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。
③ STOP: 不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程。
④ TIDYING:所有的任务都已终止了,workerCount(有效线程数)为0。
⑤ TERMINATED: 在terminated()方法执行完后进入该状态。
3.3 任务执行机制
① 任务调度
任务调度是线程池的主要入口! 当用户提交了一个任务,接下来这个任务如何执行,都是由这个阶段决定的。(了解这部分就相当于了解了线程池的核心 运行机制)
首先,所有任务的调度都是由 execute方法 完成的,这部分完成的工作是:检查现在线程池的运行状态、运行线程数、运行策略,决定接下来的执行的流程,是直接申请线程执行,或是缓冲到队列中执行,亦或是直接拒绝该任务。


② 任务缓冲
任务缓冲模块是 线程池能够管理任务 的核心部分。线程池的本质是对任务和线程的管理,而做到这一点最关键的思想就是将任务和线程两者解耦,不让两者直接关联,才可以做后续的分配工作。
线程池中是以 生产者消费者 模式:通过一个阻塞队列来实现的。阻塞队列缓存任务,工作线程从阻塞队列中获取任务。
阻塞队列描述
**阻塞队列(BlockingQueue)**是一个支持两个附加操作的队列。这两个附加的操作是:
① 在队列为空时,获取元素的线程会等待队列变为非空。
② 当队列满时,存储元素的线程会等待队列可用。
阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器拿元素。


使用不同的队列可以实现不一样的 任务存取策略。在这里,我们可以再介绍下阻塞队列的成员:
重点:阻塞队列的成员

③ 任务申请
由上文的任务分配部分可知,任务的执行有两种可能:
① 任务 直接由新创建的线程执行 。
② 线程 从任务队列中获取任务然后执行 ,执行完任务的空闲线程会再次去从队列中申请任务再去执行。
第一种情况仅出现在线程初始创建的时候,第二种是线程获取任务绝大多数的情况。


getTask 这部分进行了多次判断 ,为的是 控制线程的数量 ,使其符合线程池的状态。
如果线程池现在不应该持有那么多线程,则会返回 null 值。工作线程 Worker 会不断接收新任务去执行,而当工作线程 Worker 接收不到任务的时候,就会开始被回收。
④ 任务拒绝
任务拒绝模块是线程池的保护部分,线程池有一个最大的容量,当线程池的任务缓存队列已满,并且线程池中的线程数目达到maximumPollSize时,就需要拒绝掉该任务,采取任务拒绝策略,保护线程池。
拒绝策略接口
拒绝策略是一个接口,其设计如下:

用户可以通过实现这个接口去定制拒绝策略,也可以选择IDK提供的四种已有拒绝策略,其特点如下:

3.4 Worker 线程管理
① Worker线程
线程池为了掌握线程的状态并维护线程的⽣命周期,设计了线程池内的⼯作线程 Worker。我们来看⼀下它的部分代码:

Worker这个工作线程,实现了 Runnable接口 ,并持有一个线程 thread ,一个初始化的任务firstTask。
thread 是在调用构造方法时通过 ThreadFactory 来创建的线程 ,可以用来执行任务;
firstTask 用它来 保存传入的第一个任务 ,这个任务可以有也可以为null。
如果这个值是非空的,那么线程就会在启动初期立即执行这个任务,也就对应核心线程创建时的情况;如果这个值是null,那么就需要创建⼀个线程去执⾏任务列表(workQueue)中的任务,也就是非核心线程的创建。
Worker执行任务的模型如下图所示:

四、线程池在业务中的实践
