【观后感】Java线程池实现原理及其在美团业务中的实践

文章目录


一、前言🚀🚀🚀

☀️

回报不在行动之后,回报在行动之中。


这个系列是关于后端场景的研究探讨,本期文章参考美团技术团队《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执行任务的模型如下图所示:

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

相关推荐
ytttr8735 小时前
隐马尔可夫模型(HMM)MATLAB实现范例
开发语言·算法·matlab
天远Date Lab5 小时前
Python实战:对接天远数据手机号码归属地API,实现精准用户分群与本地化运营
大数据·开发语言·python
listhi5205 小时前
基于Gabor纹理特征与K-means聚类的图像分割(Matlab实现)
开发语言·matlab
野生的码农5 小时前
码农的妇产科实习记录
android·java·人工智能
qq_433776426 小时前
【无标题】
开发语言·php
Davina_yu6 小时前
Windows 下升级 R 语言至最新版
开发语言·windows·r语言
阿珊和她的猫6 小时前
IIFE:JavaScript 中的立即调用函数表达式
开发语言·javascript·状态模式
毕设源码-赖学姐6 小时前
【开题答辩全过程】以 高校人才培养方案管理系统的设计与实现为例,包含答辩的问题和答案
java
listhi5207 小时前
卷积码编码和维特比译码的MATLAB仿真程序
开发语言·matlab
一起努力啊~7 小时前
算法刷题-二分查找
java·数据结构·算法