进程:是计算机中的程序关于某数据集合的一次运行活动,是操作系统进行资源分配和调度的基本单位
线程:是操作系统能够进行运算调度的最小单位,一个线程就是进程中一个单一顺序的控制流,进程是线程的容器,一个进程至少有一个线程,一个进程可以有多个线程
OS中是以进程为单位分配资源,如虚拟存储控件、文件描述符。每个线程都有自己的线程栈
JVM启动时会自动创建一个主线程,该主线程负责执行main方法。主线程就是运行main方法的线程。
串行、并行、并发

在Java中,创建一个线程就是创建一个Thread类(子类)的对象(实例)
Thread类有两个常用的构造方法:Thread()与Thread(Runnable)和Callable()对应的创建线程的三种方式
currentThread()方法

在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在 Java 中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁。如何利用已有对象来服务就是一个需要解决的关键问题,其实这就是一些「池化资源」技术产生的原因。比如大家所熟悉的数据库连接池正是遵循这一思想而产生的!
线程池(Thread Pool)是一种基于池化思想管理线程的工具,经常出现在多线程服务器中,类似的还有数据库连接池、HTTP 连接池等等。
线程池解决的核心问题就是资源管理问题。
在并发环境下,系统不能够确定在任意时刻中,有多少任务需要执行,有多少资源需要投入。这种不确定性将带来以下若干问题:
为解决资源分配这个问题,线程池采用了「池化」(Pooling)思想。池化的思想主要是为了减少每次获取和结束资源的消耗,提高对资源的利用率。
Java 中的线程池核心实现类是 ThreadPoolExecutor,本文基于 JDK 1.8 的源码来分析 Java 线程池的核心设计与实现。
我们首先来看一下 ThreadPoolExecutor 的 UML 类图,了解下 ThreadPoolExecutor 的继承关系。

ThreadPoolExecutor 实现的顶层接口是 Executor。
Executor 提供了一种思想:将任务提交和任务执行进行解耦。用户无需关注如何创建线程,如何调度线程来执行任务,用户只需提供 Runnable 对象,将任务的运行逻辑提交到执行器(Executor)中,由 Executor 框架完成线程的调配和任务的执行部分。
ExecutorService 接口增加了一些能力:
AbstractExecutorService 则是上层的抽象类,将执行任务的流程串联了起来,保证下层的实现只需关注一个执行任务的方法即可。
最下层的实现类 ThreadPoolExecutor 实现最复杂的运行部分,ThreadPoolExecutor 将会一方面维护自身的生命周期,另一方面同时管理线程和任务,使两者良好的结合从而执行并行任务。
ThreadPoolExecutor 运行机制如下图所示:

线程池在内部实际上构建了一个生产者消费者模型,将线程和任务两者解耦,并不直接关联,从而良好的缓冲任务,复用线程。
线程池的运行主要分成两部分:任务管理、线程管理。
任务管理部分充当生产者的角色,当任务提交后,线程池会判断该任务后续的流转:
线程管理部分是消费者,它们被统一维护在线程池内,根据任务请求进行线程的分配,当线程执行完任务后则会继续获取新的任务去执行,最终当线程获取不到任务的时候,线程就会被回收。
线程池运行的状态,并不是用户显式设置的,而是伴随着线程池的运行,由内部来维护。线程池内部使用一个 AtomicInteger 变量维护两个值:运行状态(runState)和线程数量(workerCount)。
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
AtomicInteger:一个提供原子操作的 Integer 类。
我们知道,在不同操作系统下,Java 中的 Integer 变量都是 32 位,ThreadPoolExecutor 使用高 3 位(31 ~ 29) 表示线程池状态,用后 29 位(28 ~ 0) 表示活跃线程数。
这样设置的目的
1. 我们知道,在并发场景中同时维护两个变量的代价是非常大的,往往需要进行加锁来保证两个变量的变化是原子性的。而将两个参数用一个变量维护,便只需一条语句就能保证两个变量的原子性。这种方式大大降低了使用过程中的并发问题。
2. 通过阅读线程池源代码也可以发现,经常出现要同时判断线程池运行状态和线程数量的情况。线程池也提供了若干方法去供用户获得线程池当前的运行状态、线程个数。这里都使用的是位运算的方式,相比于基本运算,速度也会快很多。
ThreadPoolExecutor 的运行状态有 5 种,分别为:
其生命周期转换如下入所示:

任务调度是线程池的主要入口,当用户提交了一个任务,接下来这个任务将如何执行都是由这个阶段决定的。了解这部分就相当于了解了线程池的核心运行机制。
首先,在 ThreadPoolExecutor 类中,任务提交方法的入口是 execute(Runnable command) 方法(submit() 方法也是调用了 execute()),这部分完成的工作是:检查现在线程池的运行状态、运行线程数、运行策略,决定接下来执行的流程,是直接申请线程执行,或是缓冲到队列中执行,亦或是直接拒绝该任务。

未完待续
20221109-22:12
执行过程如下:
任务缓冲模块是线程池能够管理任务的核心部分。
线程池的本质是对任务和线程的管理,而做到这一点最关键的思想就是将任务和线程两者解耦,不让两者直接关联,才可以做后续的分配工作。
线程池中是以生产者消费者模式,通过一个阻塞队列来实现的。阻塞队列缓存任务,工作线程从阻塞队列中获取任务。
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:
线程 1 往阻塞队列中添加元素,而线程 2 从阻塞队列中移除元素:

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

由上文的任务分配部分可知,任务的执行有两种可能:
第一种情况仅出现在线程初始创建的时候,第二种是线程获取任务绝大多数的情况。
线程需要从任务缓存模块中不断地取任务执行,帮助线程从阻塞队列中获取任务,实现线程管理模块和任务管理模块之间的通信。
这部分策略由 getTask() 方法实现,Runnable getTask() 方法是为 void runWorker(Worker w) 方法服务的,它的作用就是在任务队列(workQueue)中获取 task(Runnable)。

getTask() 执行流程:
如果满足获取任务条件,根据是否需要定时获取调用不同方法:
在阻塞从 workQueue 中获取任务时,可以被 interrupt() 中断,代码中捕获了InterruptedException,重置 timedOut 为初始值 false,再次执行第 1 步中的判断,满足就继续获取任务,不满足 return null,会进入 worker 退出的流程。
任务拒绝模块是线程池的保护部分,线程池有一个最大的容量,当线程池的任务缓存队列已满,并且线程池中的线程数目达到 maximumPoolSize 时,就需要拒绝掉该任务,采取任务拒绝策略,保护线程池。
拒绝策略是一个接口,其设计如下:
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
用户可以通过实现这个接口去定制拒绝策略,也可以选择 JDK 提供的四种已有拒绝策略,其特点如下:


线程池为了掌握线程的状态并维护线程的生命周期,设计了线程池内的工作线程 Worker。我们来看一下它的部分代码:
private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
final Thread thread;//Worker持有的线程
Runnable firstTask;//初始化的任务,可以为null
}
Worker 这个工作线程,实现了 Runnable 接口,并持有一个线程 thread,一个初始化的任务 firstTask。
Worker 执行任务的模型如下图所示:

线程池需要管理线程的生命周期,需要在线程长时间不运行的时候进行回收。线程池使用一张 Hash 表去持有线程的引用,这样可以通过添加引用、移除引用这样的操作来控制线程的生命周期。这个时候重要的就是如何判断线程是否在运行。
Worker 是通过继承 AQS,使用 AQS 来实现独占锁这个功能。没有使用可重入锁 ReentrantLock,而是使用 AQS,为的就是实现不可重入的特性去反应线程现在的执行状态。
在线程回收过程中就使用到了这种特性,回收过程如下图所示:
增加线程是通过线程池中的 addWorker() 方法,该方法的功能就是增加一个线程,该方法不考虑线程池是在哪个阶段增加的该线程,这个分配线程的策略是在上个步骤完成的,该步骤仅仅完成增加线程,并使它运行,最后返回是否成功这个结果。

addWorker 方法有两个参数:firstTask、core。
addWorker 方法有 4 种传参的方式:
执行流程:
在 Worker 类中的 run 方法调用了 runWorker 方法来执行任务,里面是一个 while 循环,循环判断任务是否为空,若不为空,执行任务;若取不到任务,或发生异常,退出循环,执行processWorkerExit(w, completedAbruptly); 在这个方法里把工作线程移除掉。
runWorker 方法的执行过程如下:
下一步,就得看看,什么情况下 getTask() 会返回 null。
一共有两种情况会返回 null,见红框处 。

第一种情况,线程池的状态已经是 STOP,TIDYING,TERMINATED,或者是 SHUTDOWN 且工作队列为空;
第二种情况,「工作线程数已经大于最大线程数」或「当前工作线程已超时」,且,「还有其他工作线程」或「任务队列为空」。这点比较难理解,总之先记住,后面会用。
runWorker 执行流程如下图所示:

通过源码我们可以看到,在线程池提交任务后,会通过内部的 worker 线程进行任务的处理。在 runWorker() 方法中 worker 会通过循环获取任务,对于有超时限制的 worker,会在获取方法 getTask() 中进行超时判断,若判断已超时也就是空闲线程已到最大存活时间,则进行 worker 数的修改并返回空任务。
在获取到空任务时,会跳出循环进行 worker 回收的方法 processWorkerExit(),在该方法中加锁进行完成任务数的统计及 worker 从 set 中的移除,最后判断是否需要进行 worker 的补充。在实现 worker 数值修改时是通过循环 + CAS 的方式实现的。
try {
while (task != null || (task = getTask()) != null) {
//执行任务
}
} finally {
processWorkerExit(w, completedAbruptly);//获取不到任务时,主动回收自己
}
线程池中线程的销毁依赖 JVM 自动的回收,线程池做的工作是根据当前线程池的状态维护一定数量的线程引用,防止这部分线程被 JVM 回收,当线程池决定哪些线程需要回收时,只需要将其引用消除即可。

事实上,在这个方法中,将线程引用移出线程池就已经结束了线程销毁的部分。但由于引起线程销毁的可能性有很多,线程池还要判断是什么引发了这次销毁,是否要改变线程池的现阶段状态,是否要根据新状态,重新分配线程。
在当今的互联网业界,为了最大程度利用 CPU 的多核性能,并行运算的能力是不可或缺的。通过线程池管理线程获取并发性是一个非常基础的操作,让我们来看两个典型的使用线程池获取并发性的场景。
描述:用户发起的实时请求,服务追求响应时间。比如说用户要查看一个商品的信息,那么我们需要将商品维度的一系列信息如商品的价格、优惠、库存、图片等等聚合起来,展示给用户。
分析:从用户体验角度看,这个结果响应的越快越好,如果一个页面半天都刷不出,用户可能就放弃查看这个商品了。而面向用户的功能聚合通常非常复杂,伴随着调用与调用之间的级联、多级级联等情况,业务开发同学往往会选择使用线程池这种简单的方式,将调用封装成任务并行的执行,缩短总体响应时间。另外,使用线程池也是有考量的,这种场景最重要的就是获取最大的响应速度去满足用户,所以应该不设置队列去缓冲并发任务,调高 corePoolSize 和 maxPoolSize 去尽可能创造多的线程快速执行任务。

描述:离线的大量计算任务,需要快速执行。比如说,统计某个报表,需要计算出全国各个门店中有哪些商品有某种属性,用于后续营销策略的分析,那么我们需要查询全国所有门店中的所有商品,并且记录具有某属性的商品,然后快速生成报表。
分析:这种场景需要执行大量的任务,我们也会希望任务执行的越快越好。这种情况下,也应该使用多线程策略,并行计算。但与响应速度优先的场景区别在于,这类场景任务量巨大,并不需要瞬时的完成,而是关注如何使用有限的资源,尽可能在单位时间内处理更多的任务,也就是吞吐量优先的问题。所以应该设置队列去缓冲并发任务,调整合适的 corePoolSize 去设置处理任务的线程数。在这里,设置的线程数过多可能还会引发线程上下文切换频繁的问题,也会降低处理任务的速度,降低吞吐量。

线程池使用面临的核心的问题在于:线程池的参数并不好配置。
业务中要使用线程池,而使用不当又会导致故障,那么我们怎样才能更好地使用线程池呢?针对这个问题,我们下面延展几个方向。
业界的一些线程池参数配置方案:

调研了以上业界方案后,我们并没有得出通用的线程池计算方式。并发任务的执行情况和任务类型相关,IO 密集型和 CPU 密集型的任务运行起来的情况差异非常大,但这种占比是较难合理预估的,这导致很难有一个简单有效的通用公式帮我们直接计算出结果。
尽管经过谨慎的评估,仍然不能够保证一次计算出来合适的参数,那么我们是否可以将修改线程池参数的成本降下来,这样至少可以发生故障的时候可以快速调整从而缩短故障恢复的时间呢?
基于这个思考,我们是否可以将线程池的参数从代码中迁移到分布式配置中心上,实现线程池参数可动态配置和即时生效,线程池参数动态化前后的参数修改流程对比如下:

基于以上三个方向对比,我们可以看出参数动态化方向简单有效。
动态化线程池的核心设计包括以下三个方面:

动态化线程池提供如下功能:

JDK 原生线程池 ThreadPoolExecutor 提供了如下几个 public 的 setter 方法,如下图所示:

JDK 允许线程池使用方通过 ThreadPoolExecutor 的实例来动态设置线程池的核心策略,以 setCorePoolSize 为方法例,在运行期线程池使用方调用此方法设置 corePoolSize 之后,线程池会直接覆盖原来的 corePoolSize 值,并且基于当前值和原始值的比较结果采取不同的处理策略。对于当前值小于当前工作线程数的情况,说明有多余的 worker 线程,此时会向当前 idle 的 worker 线程发起中断请求以实现回收,多余的 worker 在下次 idel 的时候也会被回收;对于当前值大于原始值且当前队列中有待执行任务,则线程池会创建新的 worker 线程来执行队列任务。

线程池内部会处理好当前状态做到平滑修改,其他几个方法限于篇幅,这里不一一介绍。重点是基于这几个public方法,我们只需要维护 ThreadPoolExecutor 的实例,并且在需要修改的时候拿到实例修改其参数即可。基于以上的思路,我们实现了线程池参数的动态化、线程池参数在管理平台可配置可修改,其效果图如下图所示:

用户可以在管理平台上通过线程池的名字找到指定的线程池,然后对其参数进行修改,保存后会实时生效。目前支持的动态参数包括核心数、最大值、队列长度等。除此之外,在界面中,我们还能看到用户可以配置是否开启告警、队列等待任务告警阈值、活跃度告警等等。关于监控和告警,我们下面一节会对齐进行介绍。
除了参数动态化之外,为了更好地使用线程池,我们需要对线程池的运行状况有感知,比如:
基于对这些问题的思考,动态化线程池提供了多个维度的监控和告警能力,包括:线程池活跃度、任务的执行Transaction(频率、耗时)、Reject异常、线程池内部统计信息等等,既能帮助用户从多个维度分析线程池的使用情况,又能在出现问题第一时间通知到用户,从而避免故障或加速故障恢复。
线程池负载关注的核心问题是:基于当前线程池参数分配的资源够不够。
对于这个问题,我们可以从事前和事中两个角度来看。
事前,线程池定义了“活跃度”这个概念,来让用户在发生 Reject 异常之前能够感知线程池负载问题,线程池活跃度计算公式为:线程池活跃度 = activeCount/maximumPoolSize。
这个公式代表当活跃线程数趋向于 maximumPoolSize 的时候,代表线程负载趋高。
事中,也可以从两方面来看线程池的过载判定条件:

在传统的线程池应用场景中,线程池中的任务执行情况对于用户来说是透明的。比如在一个具体的业务场景中,业务开发申请了一个线程池同时用于执行两种任务,一个是发消息任务、一个是发短信任务,这两类任务实际执行的频率和时长对于用户来说没有一个直观的感受,很可能这两类任务不适合共享一个线程池,但是由于用户无法感知,因此也无从优化。
动态化线程池内部实现了任务级别的埋点,且允许为不同的业务任务指定具有业务含义的名称,线程池内部基于这个名称做 Transaction 打点,基于这个功能,用户可以看到线程池内部任务级别的执行情况,且区分业务,任务监控示意图如下图所示:

用户基于 JDK 原生线程池 ThreadPoolExecutor 提供的几个 public 的 getter 方法,可以读取到当前线程池的运行状态以及参数,如下图所示:
动态化线程池基于这几个接口封装了运行时状态实时查看的功能,用户基于这个功能可以了解线程池的实时状态,比如当前有多少个工作线程,执行了多少个任务,队列中等待的任务数等等。效果如下图所示:

Java线程池实现原理及其在美团业务中的实践
Java线程池ThreadPoolExecutor使用和分析(二) - execute()原理
篇幅有些长,写的时候理解了很久, 断断续续的看,一点一点的理解,只想在吹牛的时候问有所答,这篇对我来说写了太久了,发生的事情太多了,希望可以过去这道坎,重新拾起来写又要从头看,思绪也是经常被自己打乱。
。。。
。。。
。。。
,,,