一文读懂线程池的实现原理,java面试写代码

上个月底群里的一个好朋友向老周提出啥时候分享 ThreadPoolExecutor 解析大全,我说后面会提上日程;然后前些天有读者也反馈说在面试中有被问到线程池,问我啥时候出一篇线程池相关的文章。今天老周就来安排一波线程池,现在很多公司都喜欢问线程池相关的面试题,为什么面试官这么热衷于问线程池相关的面试题呢?因为这是多线程的基础,ThreadPoolExecutor 的几个重要参数你必须会知道设置以及什么场景选择哪种 Executor 、线程池队列的选择以及相应的拒绝策略。

下面老周收集了几个朋友提供的大厂关于线程池的面试题:

  • 线程池的使用场景

  • 线程池各个参数的含义,你平时用的什么队列以及拒绝策略?

  • 程序中哪些地方用到了线程池,用线程池的好处有哪些?

  • 如何自己实现一个线程池

  • JDK 提供了哪些线程池的默认实现

  • 阿里巴巴 Java 开发手册为啥不允许默认实现的线程池

  • 线程池里的参数你是怎么得出来的,根据什么算出来的?

  • 说说你自定义线程池里的工作流程

这里老周就不带大家一个个对面试题进行分析了,这里对只讲核心原理再结合动态调整线程池参数的实践来帮助你对线程池有个清晰的认识,知道了原理再结合自己的实践,那面试线程池也是得心应手了。那你有可能问,老周啊,我平时也没用到线程池啊,用的也都是定义类继承 Thread类 或者 定义类实现 Runnable 接口来实现多线程的啊。额,如果你是面的 Java 中高级开发,那你千万不要这样说,这会让面试官一下觉得你不值中高级。如果你面的中高级还不知道线程池的话也没关系,幸好你看到了老周这篇文章,还不算晚;如果你是已经用过线程池相关,那这篇文章也会让你对线程池的原理更加清楚,在项目中应用也会得心应手。

二、线程池的概念


2.1 线程池是什么

线程池是一种线程使用模式。线程过多会带来额外的开销,其中包括创建销毁线程的开销、调度线程的开销等等,同时也降低了计算机的整体性能。线程池维护多个线程,等待监督管理者分配可并发执行的任务。这种做法,一方面避免了处理任务时创建销毁线程开销的代价,另一方面避免了线程数量膨胀导致的过分调度问题,保证了对内核的充分利用。

2.2 使用线程池的好处

  • 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。

  • 提高响应速度:任务到达时,无需等待线程创建即可立即执行。

  • 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。

  • 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池 ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。

2.3、ThreadPoolExecutor 的核心参数

网上说的天花乱坠的,也不如直接看 Doug Lea 大佬源码的注释来的更加贴切些。

在这里插入图片描述

  • corePoolSize:the number of threads to keep in the pool, even if they are idle, unless {@code allowCoreThreadTimeOut} is set

核心线程数:线程池中保留的线程数,即使它们是空闲的,除非设置 allowCoreThreadTimeOut。

  • maximumPoolSize:the maximum number of threads to allow in the pool

最大线程数:线程池中允许的最大线程数

  • keepAliveTime:when the number of threads is greater than the core, this is the maximum time that excess idle threads will wait for new tasks before terminating.

线程空闲时间:如果经过 keepAliveTime 时间后,超过核心线程数的线程还没有接受到新的任务,那就回收。

  • unit:the time unit for the {@code keepAliveTime} argument

单位:keepAliveTime 的时间单位

  • workQueue:the queue to use for holding tasks before they are executed. This queue will hold only the {@code Runnable} tasks submitted by the {@code execute} method.

存放待执行任务的队列:当提交的任务数超过核心线程数后,再提交的任务就存放在这里。它仅仅用来存放被 execute 方法提交的 Runnable 任务。(这里不要再翻译成工作队列了好吗)

  • threadFactory:the factory to use when the executor creates a new thread

线程工厂:执行程序创建新线程时使用的工厂。比如我们项目中自定义的线程工厂,排查问题的时候,根据线程工厂的名称就知道这个线程来自哪里,很快的定位出问题,

  • handler :the handler to use when execution is blocked because the thread bounds and queue capacities are reached

拒绝策略:当队列里面放满了任务、最大线程数的线程都在工作时,这时继续提交的任务线程池就处理不了,应该执行怎么样的拒绝策略。

三、线程池的实现原理


本文描述线程池是 JDK 8 中提供的 ThreadPoolExecutor 类,那我们就从 ThreadPoolExecutor 类来看下它的 UML 依赖关系。

3.1 总体设计

在这里插入图片描述

  • 蓝色实线:继承关系

  • 绿色虚线:接口实现关系

  • 绿色实线:接口继承关系

ThreadPoolExecutor 实现的顶层接口是 Executor,顶层接口只提供了void execute(Runnable command); 这么一个方法,Executor 提供的是一种思想:将任务提交和任务执行进行解耦。用户无需关注如何创建线程,如何调度线程来执行任务,用户只需提供 Runnable 对象,将任务的运行逻辑提交到执行器(Executor)中,由 Executor 框架完成线程的调配和任务的执行部分。

ExecutorService 接口增加了一些能力:

  • 扩充执行任务的能力,补充可以为一个或一批异步任务生成 Future 的方法;

  • 提供了管控线程池的方法,比如停止线程池的运行。

AbstractExecutorService 则是上层的抽象类,将执行任务的流程串联了起来,保证下层的实现只需关注一个执行任务的方法即可。最下层的实现类 ThreadPoolExecutor 实现最复杂的运行部分,ThreadPoolExecutor 将会一方面维护自身的生命周期,另一方面同时管理线程和任务,使两者良好的结合从而执行并行任务。

我们来看下 ThreadPoolExecutor 的运行流程:

在这里插入图片描述

线程池在内部实际上构建了一个生产者消费者模型,将线程和任务两者解耦,并不直接关联,从而良好的缓冲任务,复用线程。线程池的运行主要分成两部分:任务管理、线程管理。

任务管理部分充当生产者的角色,当任务提交后,线程池会判断该任务后续的流转:

  • 直接申请线程执行该任务

  • 缓冲到队列中等待线程执行

  • 拒绝该任务

线程管理部分充当消费者的角色,它们被统一维护在线程池内,根据任务请求进行线程的分配,当线程执行完任务后则会继续获取新的任务去执行,最终当线程获取不到任务的时候,线程就会被回收。

下面就从以下三个核心机制来详细讲解线程池运行机制:

  • 线程池如何维护自身状态

  • 线程池如何管理任务

  • 线程池如何管理线程

3.2 线程池如何维护自身状态

线程池运行的状态,并不是用户显式设置的,而是伴随着线程池的运行,由内部来维护。线程池内部使用一个变量维护两个值:运行状态(runState)和线程数量(workerCount)。

在这里插入图片描述

ctl 这个 AtomicInteger 类型,是对线程池的运行状态和线程池中有效线程的数量进行控制的一个字段, 它同时包含两部分的信息:线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount),高 3 位保存 runState,低 29 位保存 workerCount,两个变量之间互不干扰。用一个变量去存储两个值,可避免在做相关决策时,出现不一致的情况,不必为了维护两者的一致,而占用锁资源。通过阅读线程池源代码也可以发现,经常出现要同时判断线程池运行状态和线程数量的情况。线程池也提供了若干方法去供用户获得线程池当前的运行状态、线程个数。这里都使用的是位运算的方式,相比于基本运算,速度也会快很多。

关于内部封装的获取生命周期状态、获取线程池线程数量的计算方法如下代码:

在这里插入图片描述

哇,Doug Lea 大佬简直了,设计的真好。老周等等我,这里怎么设计的就好了?CAPACITY 这里是多少呀?

不着急,老周这就带你来分析分析为什么一个整型变量既可以保存运行状态,又可以保存线程数量?

首先,我们知道 Java 中 1 个整型占 4 个字节,也就是 32 位,所以 1 个整型有 32 位。

所以整型 1 用二进制表示就是:0000 0000 0000 0000 0000 0000 0000 0001

整型 -1 用二进制表示就是:1111 1111 1111 1111 1111 1111 1111 1111 (这个是补码,这个忘了的话那得去复习下原码、反码、补码等计算机基础知识了。)

在 ThreadPoolExecutor,整型中 32 位的前 3 位用来表示线程池状态,后 29 位表示线程池中有效的线程数。

在这里插入图片描述

这里你有可能问了,老周啊,CAPACITY = (1 << 29) - 1 怎么就得到 0001 1111 1111 1111 1111 1111 1111 1111。

好吧,老周就带你分析下 CAPACITY 怎么来的,下面的那些状态大家也可以自己去分析下哈。

我们先来看 1 << 29,首先看 1 的二进制代表 0000 0000 0000 0000 0000 0000 0000 0001。

然后 0000 0000 0000 0000 0000 0000 0000 0001 向左移 29 位,得到 0010 0000 0000 0000 0000 0000 0000 0000。

最后将 0010 0000 0000 0000 0000 0000 0000 0000 减 1 得到 0001 1111 1111 1111 1111 1111 1111 1111。

我们下面再来了解下 ThreadPoolExecutor 所定义的状态,这些状态都和线程的执行密切相关:

在这里插入图片描述

  • RUNNING:能接受新提交的任务,并且也能处理阻塞队列中的任务。

  • SHUTDOWN:指调用了 shutdown() 方法,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。

  • STOP:指调用了 shutdownNow() 方法,不再接受新提交的任务,同时抛弃阻塞队列里的所有任务并中断所有正在执行任务。

  • TIDYING: 所有任务都执行完毕,workerCount 有效线程数为 0。

  • TERMINATED:终止状态,当执行 terminated() 后会更新为这个状态。

在这里插入图片描述

3.3 线程池如何管理任务

3.3.1 任务调度

任务调度是线程池的主要入口,当用户提交了一个任务,接下来这个任务将如何执行都是由这个阶段决定的。了解这部分就相当于了解了线程池的核心运行机制。

首先,所有任务的调度都是由 execute 方法完成的,比如我们业务代码中

threadPool.execute(new Job());

这部分完成的工作是:检查现在线程池的运行状态、运行线程数、运行策略,决定接下来执行的流程,是直接申请线程执行,或是缓冲到队列中执行,亦或是直接拒绝该任务。其执行过程如下:

  • 首先检测线程池运行状态,如果不是 RUNNING,则直接拒绝,线程池要保证在 RUNNING 的状态下执行任务。

  • 如果 workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。

  • 如果 workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。

  • 如果 workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。

  • 如果 workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满,则根据拒绝策略来处理该任务,默认的处理方式是直接抛异常。

执行流程图如下:

在这里插入图片描述

3.3.2 待执行任务的队列

待执行任务的队列是线程池能够管理任务的核心部分。线程池的本质是对任务和线程的管理,而做到这一点最关键的思想就是将任务和线程两者解耦,不让两者直接关联,才可以做后续的分配工作。线程池中是以生产者消费者模式,通过一个阻塞队列来实现的。阻塞队列缓存任务,工作线程从阻塞队列中获取任务。

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。

这两个附加的操作是:

  • 在队列为空时,获取元素的线程会等待队列变为非空。

  • 当队列满时,存储元素的线程会等待队列可用。

阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

下图中展示了 Thread1 往阻塞队列中添加元素,而线程 Thread2 从阻塞队列中移除元素:

在这里插入图片描述

使用不同的队列可以实现不一样的任务存取策略。我们下面来看下阻塞队列的成员:

在这里插入图片描述

3.3.3 任务申请

从上文可知,任务的执行有两种可能:

  • 一种是任务直接由新创建的线程执行

  • 另一种是线程从任务队列中获取任务然后执行,执行完任务的空闲线程会再次去从队列中申请任务再去执行。

第一种情况仅出现在线程初始创建的时候,第二种是线程获取任务绝大多数的情况。

线程需要从待执行任务的队列中不断地取任务执行,帮助线程从阻塞队列中获取任务,实现线程管理模块和任务管理模块之间的通信。

这部分策略由 getTask 方法实现,我们来看下 getTask 方法的代码。

在这里插入图片描述

getTask 方法在阻塞队列中有待执行的任务时会从队列中弹出一个任务并返回,如果阻塞队列为空,那么就会阻塞等待新的任务提交到队列中直到超时(在一些配置下会一直等待而不超时),如果在超时之前获取到了新的任务,那么就会将这个任务作为返回值返回。所以一般 getTask 方法是不会返回 null 的,只会阻塞等待下一个任务并在之后将这个新任务作为返回值返回。

当 getTask 方法返回 null 时会导致当前 Worker 退出,当前线程被销毁。在以下情况下 getTask 方法才会返回 null:

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
img

言尽于此,完结

无论是一个初级的 coder,高级的程序员,还是顶级的系统架构师,应该都有深刻的领会到设计模式的重要性。

  • 第一,设计模式能让专业人之间交流方便,如下:

程序员A:这里我用了XXX设计模式

程序员B:那我大致了解你程序的设计思路了

  • 第二,易维护

项目经理:今天客户有这样一个需求…

程序员:明白了,这里我使用了XXX设计模式,所以改起来很快

  • 第三,设计模式是编程经验的总结

程序员A:B,你怎么想到要这样去构建你的代码

程序员B:在我学习了XXX设计模式之后,好像自然而然就感觉这样写能避免一些问题

  • 第四,学习设计模式并不是必须的

程序员A:B,你这段代码使用的是XXX设计模式对吗?

程序员B:不好意思,我没有学习过设计模式,但是我的经验告诉我是这样写的

image

从设计思想解读开源框架,一步一步到Spring、Spring5、SpringMVC、MyBatis等源码解读,我都已收集整理全套,篇幅有限,这块只是详细的解说了23种设计模式,整理的文件如下图一览无余!

image

搜集费时费力,能看到此处的都是真爱!
维护

项目经理:今天客户有这样一个需求…

程序员:明白了,这里我使用了XXX设计模式,所以改起来很快

  • 第三,设计模式是编程经验的总结

程序员A:B,你怎么想到要这样去构建你的代码

程序员B:在我学习了XXX设计模式之后,好像自然而然就感觉这样写能避免一些问题

  • 第四,学习设计模式并不是必须的

程序员A:B,你这段代码使用的是XXX设计模式对吗?

程序员B:不好意思,我没有学习过设计模式,但是我的经验告诉我是这样写的

[外链图片转存中…(img-Y8oaOgBP-1712048520398)]

从设计思想解读开源框架,一步一步到Spring、Spring5、SpringMVC、MyBatis等源码解读,我都已收集整理全套,篇幅有限,这块只是详细的解说了23种设计模式,整理的文件如下图一览无余!

[外链图片转存中…(img-vc50n1lO-1712048520399)]

搜集费时费力,能看到此处的都是真爱!

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值