如果你是 JDK 设计者,如何设计线程池?我跟面试官大战了三十个回合(下)

简介: 如果你是 JDK 设计者,如何设计线程池?我跟面试官大战了三十个回合(下)

这里可以看到,Tomcat 维护了一个 submittedCount 变量,这个变量的含义是统计已经提交的但是还未完成的任务数量(记住这个变量,很关键),所以只要提交一个任务,这个数就加一,并且捕获了拒绝异常,再次尝试将任务入队,这个操作其实是为了尽可能的挽救回一些任务,因为这么点时间差可能已经执行完很多任务,队列腾出了空位,这样就不需要丢弃任务。


然后我们再来看下代码里出现的 TaskQueue,这个就是上面提到的定制关键点了。

public class TaskQueue extends LinkedBlockingQueue<Runnable> {
    private transient volatile ThreadPoolExecutor parent = null;
    ........
}


可以看到这个任务队列继承了 LinkedBlockingQueue,并且有个 ThreadPoolExecutor 类型的成员变量 parent ,我们再来看下 offer 方法的实现,这里就是修改原来线程池任务提交与线程创建逻辑的核心了。


image.png


从上面的逻辑可以看出是有机会在队列还未满的时候,先创建线程至最大线程数的!

再补充一下,如果对直接返回 false 就能创建线程感到疑惑的话,往上翻一翻,上面贴了原生线程池 execute 的逻辑。

然后上面的代码其实只看到 submittedCount 的增加,正常的减少在 afterExecute 里实现了。


image.png


image.png


至此,想必 Tomcat 中的定制化线程池的逻辑已经明白了。

如果线程池中的线程在执行任务的时候,抛异常了,会怎么样?

嘿嘿,细心的同学想必已经瞄到了上面的代码,task.run() 被 try catch finally包裹,异常被扔到了 afterExecute 中,并且也继续被抛了出来。

而这一层外面,还有个try  finally,所以异常的抛出打破了 while 循环,最终会执行 `processWorkerExit方法


image.png


image.png


移除了引用等于销毁了,这事儿 GC 会做的。

所以如果一个任务执行一半就抛出异常,并且你没有自行处理这个异常,那么这个任务就这样戛然而止了,后面也不会有线程继续执行剩下的逻辑,所以要自行捕获和处理业务异常。

addWorker 的逻辑就不分析了,就是新建一个线程,然后塞到 workers 里面,然后调用 start() 让它跑起来。

原生线程池的核心线程一定伴随着任务慢慢创建的吗?

并不是,线程池提供了两个方法:

  • prestartCoreThread:启动一个核心线程
  • prestartAllCoreThreads :启动所有核心线程

不要小看这个预创建方法,预热很重要,不然刚重启的一些服务有时是顶不住瞬时请求的,就立马崩了,所以有预热线程、缓存等等操作。

线程池的核心线程在空闲的时候一定不会被回收吗?

有个 allowCoreThreadTimeOut 方法,把它设置为 true ,则所有线程都会超时,不会有核心数那条线的存在。

具体是会调用 interruptIdleWorkers 这个方法。


image.png


这里需要讲一下的是 w.tryLock() 这个方法,有些人可能会奇怪,Worker 怎么还能 lock。

Worker 是属于工作线程的封装类,它不仅实现了 Runnable 接口,还继承了 AQS。


image.png


之所以要继承 AQS 就是为了用上 lock 的状态,执行任务的时候上锁,任务执行完了之后解锁,这样执行关闭线程池等操作的时候可以通过 tryLock 来判断此时线程是否在干活,如果 tryLock 成功说明此时线程是空闲的,可以安全的回收。

interruptIdleWorkers 对应的还有一个  interruptWorkers 方法,从名字就能看出差别,不空闲的 worker 也直接给打断了。

根据这两个方法,又可以扯到 shutdown 和 shutdownNow,就是关闭线程池的方法,一个是安全的关闭线程池,会等待任务都执行完毕,一个是粗暴的直接咔嚓了所有线程,管你在不在运行,两个方法分别调用的就是 interruptIdleWorkers() 和 interruptWorkers() 来中断线程。


image.png


这又可以引申出一个问题,shutdownNow 了之后还在任务队列中的任务咋办?眼尖的小伙伴应该已经看到了,线程池还算负责,把未执行的任务拖拽到了一个列表中然后返回,至于怎么处理,就交给调用者了!

详细就是上面的 drainQueue 方法。


image.png


这里可能又会有同学有疑问,都 drainTo 了,为什么还要判断一下队列是否为空,然后进行循环?

那是因为如果队列是 DelayQueue 或任何其他类型的队列,其中 poll 或 drainTo 可能无法删除某些元素,所以需要遍历,逐个删除它们。


回到最开始的面试题


线程池如何动态修改核心线程数和最大线程数?

其实之所以会有这样的需求是因为线程数是真的不好配置。

你可能会在网上或者书上看到很多配置公式,比如:

  • CPU 密集型的话,核心线程数设置为 CPU核数+1
  • I/O 密集型的话,核心线程数设置为 2*CPU核数

比如:

线程数=CPU核数 *(1+线程等待时间 / 线程时间运行时间)

这个比上面的更贴合与业务,还有一些理想的公式就不列了。就这个公式而言,这个线程等待时间就很难测,拿 Tomcat 线程池为例,每个请求的等待时间能知道?不同的请求不同的业务,就算相同的业务,不同的用户数据量也不同,等待时间也不同。

所以说线程数真的很难通过一个公式一劳永逸,线程数的设定是一个迭代的过程,需要压测适时调整,以上的公式做个初始值开始调试是 ok 的。

再者,流量的突发性也是无法判断的,举个例子 1 秒内一共有 1000 个请求量,但是如果这 1000 个请求量都是在第一毫秒内瞬时进来的呢?

这就很需要线程池的动态性,也是这个上面这个面试题的需求来源。

原生的线程池核心我们大致都过了一遍,不过这几个方法一直没提到,先来看看这几个方法:

image.png


我就不一一翻译了,大致可以看出线程池其实已经给予方法暴露出内部的一些状态,例如正在执行的线程数、已完成的任务数、队列中的任务数等等。

当然你可以想要更多的数据监控都简单的,像 Tomcat 那种继承线程池之后自己加呗,动态调整的第一步监控就这样搞定了!定时拉取这些数据,然后搞个看板,再结合邮件、短信、钉钉等报警方式,我们可以很容易的监控线程池的状态!

接着就是动态修改线程池配置了。


image.png

可以看到线程池已经提供了诸多修改方法来更改线程池的配置,所以李老都已经考虑到啦!

同样,也可以继承线程池增加一些方法来修改,看具体的业务场景了。同样搞个页面,然后给予负责人员配置修改即可。

所以原生线程池已经提供修改配置的方法,也对外暴露出线程池内部执行情况,所以只要我们实时监控情况,调用对应的 set 方法,即可动态修改线程池对应配置。

回答面试题的时候一定要提监控,显得你是有的放矢的。

如果你是 JDK 设计者,如何设计?

其实我觉得这个是紧接着上一题问的,应该算是同一个问题。

而且 JDK 设计者已经设计好了呀?所以怎么说我也不清楚,不过我们可以说一说具体的实现逻辑呗。

先来看下设置核心线程数的方法:


image.png


随着注释看下来应该没什么问题,就是那个 k 值我说一下,李老觉得核心线程数是配置了,但是此时线程池内部是否需要这么多线程是不确定的,那么就按工作队列里面的任务数来,直接按任务数立刻新增线程,当任务队列为空了之后就终止新增。

这其实和李老设计的默认核心线程数增加策略是一致的,都是懒创建线程。

再看看设置最大线程数的方法:


image.png


没啥花头,调用的 interruptIdleWorkers 之前都分析过了。

我再贴一下之前写过的线程池设计面试题吧。

如果要让你设计一个线程池,你要怎么设计?

这种设计类问题还是一样,先说下理解,表明你是知道这个东西的用处和原理的,然后开始 BB。基本上就是按照现有的设计来说,再添加一些个人见解。

线程池讲白了就是存储线程的一个容器,池内保存之前建立过的线程来重复执行任务,减少创建和销毁线程的开销,提高任务的响应速度,并便于线程的管理。

我个人觉得如果要设计一个线程池的话得考虑池内工作线程的管理、任务编排执行、线程池超负荷处理方案、监控。

初始化线程数、核心线程数、最大线程池都暴露出来可配置,包括超过核心线程数的线程空闲消亡配置。

任务的存储结构可配置,可以是无界队列也可以是有界队列,也可以根据配置分多个队列来分配不同优先级的任务,也可以采用 stealing 的机制来提高线程的利用率。

再提供配置来表明此线程池是 IO 密集还是 CPU 密集型来改变任务的执行策略。

超负荷的方案可以有多种,包括丢弃任务、拒绝任务并抛出异常、丢弃最旧的任务或自定义等等。

线程池埋好点暴露出用于监控的接口,如已处理任务数、待处理任务数、正在运行的线程数、拒绝的任务数等等信息。

我觉得基本上这样答就差不多了,等着面试官的追问就好。

注意不需要跟面试官解释什么叫核心线程数之类的,都懂的没必要。

当然这种开放型问题还是仁者见仁智者见智,我这个不是标准答案,仅供参考。

关于线程池的一点碎碎念

线程池的好处我们都知道了,但是不是任何时刻都上线程池的,我看过一些奇怪的代码,就是为了用线程池而用线程池...

还有需要根据不同的业务划分不同的线程池,不然会存在一些耗时的业务影响了另一个业务导致这个业务崩了,然后都崩了的情况,所以要做好线程池隔离。


最后


好了,有关线程池的知识点和一些常见的一些面试题应该都涉及到了吧,如果还有别的啥角度刁钻的面试题,欢迎留言提出,咱们一起研究研究。

相信看了这篇文章之后,关于线程池出去面试可以开始吹了!

如果觉得文章不错,来个点赞和在看呗!

微信搜索「yes的练级攻略」关注公众号后,在后台回复 算法 ,领取这个算法笔记,很完备,很贴心,1730 页,20W 字,汇总了各大题型,基本上每题都附上了各大佬们的高质量解题思路,并附上链接,方便查看原文


image.png

image.png

相关文章
|
26天前
|
Java 程序员
java线程池讲解面试
java线程池讲解面试
49 1
|
2月前
|
Java 调度 开发者
JDK 21中的虚拟线程:轻量级并发的新篇章
本文深入探讨了JDK 21中引入的虚拟线程(Virtual Threads)概念,分析了其背后的设计哲学,以及与传统线程模型的区别。文章还将讨论虚拟线程如何简化并发编程,提高资源利用率,并展示了一些使用虚拟线程进行开发的示例。
|
4月前
|
Java
面试1: 解决线程顺序有几种方式
面试1: 解决线程顺序有几种方式
|
5月前
|
算法 安全 Java
【Java】JDK 21中的虚拟线程以及其他新特性
【Java】JDK 21中的虚拟线程以及其他新特性
129 0
|
7月前
|
存储 SQL 安全
Android面试中问的线程相关问题
Android面试中问的线程相关问题
40 0
|
7月前
|
存储 安全 Java
杰哥教你面试之一百问系列:java中高级多线程concurrent的使用
提到多线程,当然要熟悉java提供的各种多线程相关的并发包了,而java.util.concurrent就是最最经常会使用到的,那么关于concurrent的面试题目有哪些呢?一起来看看吧。
杰哥教你面试之一百问系列:java中高级多线程concurrent的使用
|
3月前
|
调度
【面试问题】说说线程的生命周期?
【1月更文挑战第27天】【面试问题】说说线程的生命周期?
|
7天前
|
调度 Python
Python多线程、多进程与协程面试题解析
【4月更文挑战第14天】Python并发编程涉及多线程、多进程和协程。面试中,对这些概念的理解和应用是评估候选人的重要标准。本文介绍了它们的基础知识、常见问题和应对策略。多线程在同一进程中并发执行,多进程通过进程间通信实现并发,协程则使用`asyncio`进行轻量级线程控制。面试常遇到的问题包括并发并行混淆、GIL影响多线程性能、进程间通信不当和协程异步IO理解不清。要掌握并发模型,需明确其适用场景,理解GIL、进程间通信和协程调度机制。
27 0
|
1月前
|
消息中间件 存储 算法
【C/C++ 泡沫精选面试题04】在实际项目中,多进程和多线程如何选择?
【C/C++ 泡沫精选面试题04】在实际项目中,多进程和多线程如何选择?
43 1
|
1月前
|
Java 调度
金三银四面试必问:线程有几种状态
金三银四面试必问:线程有几种状态
14 0