如果你是 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

相关文章
|
监控 Kubernetes Java
阿里面试:5000qps访问一个500ms的接口,如何设计线程池的核心线程数、最大线程数? 需要多少台机器?
本文由40岁老架构师尼恩撰写,针对一线互联网企业的高频面试题“如何确定系统的最佳线程数”进行系统化梳理。文章详细介绍了线程池设计的三个核心步骤:理论预估、压测验证和监控调整,并结合实际案例(5000qps、500ms响应时间、4核8G机器)给出具体参数设置建议。此外,还提供了《尼恩Java面试宝典PDF》等资源,帮助读者提升技术能力,顺利通过大厂面试。关注【技术自由圈】公众号,回复“领电子书”获取更多学习资料。
|
数据采集 Java Linux
面试大神教你:如何巧妙回答线程优先级这个经典考题?
大家好,我是小米。本文通过故事讲解Java面试中常见的线程优先级问题。小明和小华的故事帮助理解线程优先级:高优先级线程更可能被调度执行,但并非越高越好。实际开发需权衡业务需求,合理设置优先级。掌握线程优先级不仅能写出高效代码,还能在面试中脱颖而出。最后,小张因深入分析成功拿下Offer。希望这篇文章能助你在面试中游刃有余!
347 4
面试大神教你:如何巧妙回答线程优先级这个经典考题?
|
Java 程序员 开发者
Java社招面试题:一个线程运行时发生异常会怎样?
大家好,我是小米。今天分享一个经典的 Java 面试题:线程运行时发生异常,程序会怎样处理?此问题考察 Java 线程和异常处理机制的理解。线程发生异常,默认会导致线程终止,但可以通过 try-catch 捕获并处理,避免影响其他线程。未捕获的异常可通过 Thread.UncaughtExceptionHandler 处理。线程池中的异常会被自动处理,不影响任务执行。希望这篇文章能帮助你深入理解 Java 线程异常处理机制,为面试做好准备。如果你觉得有帮助,欢迎收藏、转发!
930 14
|
算法 安全 Java
Java线程调度揭秘:从算法到策略,让你面试稳赢!
在社招面试中,关于线程调度和同步的相关问题常常让人感到棘手。今天,我们将深入解析Java中的线程调度算法、调度策略,探讨线程调度器、时间分片的工作原理,并带你了解常见的线程同步方法。让我们一起破解这些面试难题,提升你的Java并发编程技能!
802 16
|
缓存 安全 Java
面试中的难题:线程异步执行后如何共享数据?
本文通过一个面试故事,详细讲解了Java中线程内部开启异步操作后如何安全地共享数据。介绍了异步操作的基本概念及常见实现方式(如CompletableFuture、ExecutorService),并重点探讨了volatile关键字、CountDownLatch和CompletableFuture等工具在线程间数据共享中的应用,帮助读者理解线程安全和内存可见性问题。通过这些方法,可以有效解决多线程环境下的数据共享挑战,提升编程效率和代码健壮性。
526 6
|
安全 Java 程序员
面试直击:并发编程三要素+线程安全全攻略!
并发编程三要素为原子性、可见性和有序性,确保多线程操作的一致性和安全性。Java 中通过 `synchronized`、`Lock`、`volatile`、原子类和线程安全集合等机制保障线程安全。掌握这些概念和工具,能有效解决并发问题,编写高效稳定的多线程程序。
590 11
|
Java Linux 调度
硬核揭秘:线程与进程的底层原理,面试高分必备!
嘿,大家好!我是小米,29岁的技术爱好者。今天来聊聊线程和进程的区别。进程是操作系统中运行的程序实例,有独立内存空间;线程是进程内的最小执行单元,共享内存。创建进程开销大但更安全,线程轻量高效但易引发数据竞争。面试时可强调:进程是资源分配单位,线程是CPU调度单位。根据不同场景选择合适的并发模型,如高并发用线程池。希望这篇文章能帮你更好地理解并回答面试中的相关问题,祝你早日拿下心仪的offer!
613 6
|
缓存 安全 算法
Java 多线程 面试题
Java 多线程 相关基础面试题
367 2
|
存储 缓存 安全
【Java面试题汇总】多线程、JUC、锁篇(2023版)
线程和进程的区别、CAS的ABA问题、AQS、哪些地方使用了CAS、怎么保证线程安全、线程同步方式、synchronized的用法及原理、Lock、volatile、线程的六个状态、ThreadLocal、线程通信方式、创建方式、两种创建线程池的方法、线程池设置合适的线程数、线程安全的集合?ConcurrentHashMap、JUC
【Java面试题汇总】多线程、JUC、锁篇(2023版)
|
消息中间件 前端开发 NoSQL
面试官:线程池遇到未处理的异常会崩溃吗?
面试官:线程池遇到未处理的异常会崩溃吗?
358 4
面试官:线程池遇到未处理的异常会崩溃吗?