面试官:线程池如何按照core、max、queue的执行顺序去执行?
wptr33 2025-09-19 03:56 1 浏览
前言
这是一个真实的面试题。
前几天一个朋友在群里分享了他刚刚面试候选者时问的问题:"线程池如何按照core、max、queue的执行循序去执行?"。
我们都知道线程池中代码执行顺序是:corePool->workQueue->maxPool,源码我都看过,你现在问题让我改源码??
一时间群里炸开了锅,小伙伴们纷纷打听他所在的公司,然后拉黑避坑。(手动狗头,大家一起调侃())
关于线程池他一共问了这么几个问题:
- 线程池如何按照core、max、queue的顺序去执行?
- 子线程抛出的异常,主线程能感知到么?
- 线程池发生了异常该怎样处理?
全是一些有意思的问题,我之前也写过一篇很详细的图文教程:【万字图文-原创】 | 学会Java中的线程池,这一篇也许就够了! ,不了解的小伙伴可以再回顾下~
但是针对这几个问题,可能大家一时间也有点懵。今天的文章我们以源码为基础来分析下该如何回答这三个问题。(之前没阅读过源码也没关系,所有的分析都会贴出源码及图解)
线程池如何按照core、max、queue的顺序执行?
问题思考
对于这个问题,很多小伙伴肯定会疑惑:"别人源码中写好的执行流程你为啥要改?这面试官脑子有病吧……"
这里来思考一下现实工作场景中是否有这种需求?之前也看到过一份简历也写到过这个问题:
场景描述.png
一个线程池执行的任务属于IO密集型,CPU大多属于闲置状态,系统资源未充分利用。如果一瞬间来了大量请求,如果线程池数量大于coreSize时,多余的请求都会放入到等待队列中。等待着corePool中的线程执行完成后再来执行等待队列中的任务。
试想一下,这种场景我们该如何优化?
我们可以修改线程池的执行顺序为corePool->maxPool->workQueue。 这样就能够充分利用CPU资源,提交的任务会被优先执行。当线程池中线程数量大于maxSize时才会将任务放入等待队列中。
你就说巧不巧?面试官的这个问题显然是经过认真思考来提问的,这是一个很有意思的问题,下面就一起看看如何解决吧。
线程池运行流程
我们都知道线程池执行流程是先corePool再workQueue,最后才是maxPool的一个执行流程。
执行流程.png
线程池核心参数
在回顾下
ThreadPoolExecutor.execute()源码前我们先回顾下线程池中的几个重要参数:
线程池核心参数.png
我们来看下这几个参数的定义:corePoolSize: 线程池中核心线程数量maximumPoolSize: 线程池中最大线程数量keepAliveTime: 非核心的空闲线程等待新任务的时间 unit: 时间单位。配合allowCoreThreadTimeOut也会清理核心线程池中的线程。workQueue: 基于Blocking的任务队列,最好选用有界队列,指定队列长度threadFactory: 线程工厂,最好自定义线程工厂,可以自定义每个线程的名称handler: 拒绝策略,默认是AbortPolicy
ThreadPoolExecutor.execute()源码分析
我们可以看下execute()如下:
execute执行源码.png
接着来分析下执行过程:
- 第一步:workerCountOf(c)时间计算当前线程池中线程的个数,当线程个数小于核心线程数
- 第二步:线程池线程数量大于核心线程数,此时提交的任务会放入workQueue中,使用offer()进行操作
- 第三步:workQueue.offer()执行失败,新提交的任务会直接执行,addWorker()会判断如果当前线程池数量大于最大线程数,则执行拒绝策略
好了,到了这里我们都已经很清楚了,关键在于第二步和第三步如何交换顺序执行呢?
解决思路
仔细想一想,如果修改workQueue.offer()的实现不就可以达到目的了?我们先来画图来看一下:
问题思路.png
现在的问题就在于,如果当前线程池中coreSize < workCount < maxSize时,一定会先执行offer()操作。
我们如果修改offer的实现是否可以完成执行顺序的更换呢?这里也是画图来展示一下:
解决方式.png
Dubbo中EagerThreadPool解决方案
凑巧Dubbo中也有类似的实现,在Dubbo的EagerThreadPool自定义了一个BlockingQueue,在offer()方法中,如果当前线程池数量小于最大线程池时,直接返回false,这里就达到了调节线程池执行顺序的目的。
dubbo中解决方案.png
源码直达:
https://github.com/apache/dubbo/blob/master/dubbo-common/src/main/java/org/apache/dubbo/common/threadpool/support/eager/TaskQueue.java
看到这里一切都真相大白了,解决思路以及方案都很简单,学会了没有?
这个问题背后还隐藏了一些场景的优化、源码的扩展等等知识,果然是一个值得思考的好问题。
子线程抛出的异常,主线程能感知到么?
问题思考
这个问题其实也很容易回答,也仅仅是一个面试题而已,实际工作中子线程的异常不应该由主线程来捕获。
针对这个问题,希望大家清楚的是: 我们要明确线程代码的边界,异步化过程中,子线程抛出的异常应该由子线程自己去处理,而不是需要主线程感知来协助处理。
解决方案
解决方案很简单,在虚拟机中,当一个线程如果没有显式处理异常而抛出时会将该异常事件报告给该线程对象的
java.lang.Thread.UncaughtExceptionHandler 进行处理,如果线程没有设置 UncaughtExceptionHandler,则默认会把异常栈信息输出到终端而使程序直接崩溃。
所以如果我们想在线程意外崩溃时做一些处理就可以通过实现 UncaughtExceptionHandler 来满足需求。
我们使用线程池设置ThreadFactory时可以指定UncaughtExceptionHandler,这样就可以捕获到子线程抛出的异常了。
代码示例
具体代码如下:
/**
* 测试子线程异常问题
*
* @author wangmeng
* @date 2020/6/13 18:08
*/
public class ThreadPoolExceptionTest {
public static void main(String[] args) throws InterruptedException {
MyHandler myHandler = new MyHandler();
ExecutorService execute = new ThreadPoolExecutor(10, 10,
0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), new ThreadFactoryBuilder().setUncaughtExceptionHandler(myHandler).build());
TimeUnit.SECONDS.sleep(5);
for (int i = 0; i < 10; i++) {
execute.execute(new MyRunner());
}
}
private static class MyRunner implements Runnable {
@Override
public void run() {
int count = 0;
while (true) {
count++;
System.out.println("我要开始生产Bug了============");
if (count == 10) {
System.out.println(1 / 0);
}
if (count == 20) {
System.out.println("这里是不会执行到的==========");
break;
}
}
}
}
}
class MyHandler implements Thread.UncaughtExceptionHandler {
private final static Logger LOGGER = LoggerFactory.getLogger(MyHandler.class);
@Override
public void uncaughtException(Thread t, Throwable e) {
LOGGER.error("threadId = {}, threadName = {}, ex = {}", t.getId(), t.getName(), e.getMessage());
}
}
执行结果:
执行结果.png
UncaughtExceptionHandler 解析
我们来看下Thread中的内部接口UncaughtExceptionHandler:
public class Thread {
......
/**
* 当一个线程因未捕获的异常而即将终止时虚拟机将使用 Thread.getUncaughtExceptionHandler()
* 获取已经设置的 UncaughtExceptionHandler 实例,并通过调用其 uncaughtException(...) 方
* 法而传递相关异常信息。
* 如果一个线程没有明确设置其 UncaughtExceptionHandler,则将其 ThreadGroup 对象作为其
* handler,如果 ThreadGroup 对象对异常没有什么特殊的要求,则 ThreadGroup 会将调用转发给
* 默认的未捕获异常处理器(即 Thread 类中定义的静态未捕获异常处理器对象)。
*
* @see #setDefaultUncaughtExceptionHandler
* @see #setUncaughtExceptionHandler
* @see ThreadGroup#uncaughtException
*/
@FunctionalInterface
public interface UncaughtExceptionHandler {
/**
* 未捕获异常崩溃时回调此方法
*/
void uncaughtException(Thread t, Throwable e);
}
/**
* 静态方法,用于设置一个默认的全局异常处理器。
*/
public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
defaultUncaughtExceptionHandler = eh;
}
/**
* 针对某个 Thread 对象的方法,用于对特定的线程进行未捕获的异常处理。
*/
public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
checkAccess();
uncaughtExceptionHandler = eh;
}
/**
* 当 Thread 崩溃时会调用该方法获取当前线程的 handler,获取不到就会调用 group(handler 类型)。
* group 是 Thread 类的 ThreadGroup 类型属性,在 Thread 构造中实例化。
*/
public UncaughtExceptionHandler getUncaughtExceptionHandler() {
return uncaughtExceptionHandler != null ?
uncaughtExceptionHandler : group;
}
/**
* 线程全局默认 handler。
*/
public static UncaughtExceptionHandler getDefaultUncaughtExceptionHandler() {
return defaultUncaughtExceptionHandler;
}
......
}
部分内容参考自:
https://mp.weixin.qq.com/s/ghnNQnpou6-NemhFjpl4Jg
线程池发生了异常该怎样处理?
线程池中线程运行过程中出现了异常该怎样处理呢?线程池提交任务有两种方式,分别是execute()和submit(),这里会依次说明。
ThreadPoolExecutor.runWorker()实现
不管是使用execute()还是submit()提交任务,最终都会执行到
ThreadPoolExecutor.runWorker(),我们来看下源码(源码基于JDK1.8):
runWorker().png
我们看到在执行task.run()时,出现异常会直接向上抛出,这里处理的最好的方式就是在我们业务代码中使用try...catch()来捕获异常。
FutureTask.run()实现
如果我们使用submit()来提交任务,在
ThreadPoolExecutor.runWorker()方法执行时最终会调用到FutureTask.run()方法里面去,不清楚的小伙伴也可以看下我之前的文章:
线程池续:你必须要知道的线程池submit()实现原理之FutureTask!
FutureTask.run().png
这里可以看到,如果业务代码抛出异常后,会被catch捕获到,然后调用setExeception()方法:
FutureTask.setException().png
可以看到其实类似于直接吞掉了,当我们调用get()方法的时候异常信息会包装到FutureTask内部的变量outcome中,我们也会获取到对应的异常信息。
在
ThreadPoolExecutor.runWorker()最后finally中有一个afterExecute()钩子方法,如果我们重写了afterExecute()方法,就可以获取到子线程抛出的具体异常信息Throwable了。
结论
对于线程池、包括线程的异常处理推荐以下方式:
- 直接使用try/catch,这个也是最推荐的方式
- 在我们构造线程池的时候,重写uncaughtException()方法,上面示例代码也有提到:
public class ThreadPoolExceptionTest {
public static void main(String[] args) throws InterruptedException {
MyHandler myHandler = new MyHandler();
ExecutorService execute = new ThreadPoolExecutor(10, 10,
0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), new ThreadFactoryBuilder().setUncaughtExceptionHandler(myHandler).build());
TimeUnit.SECONDS.sleep(5);
for (int i = 0; i < 10; i++) {
execute.execute(new MyRunner());
}
}
}
class MyHandler implements Thread.UncaughtExceptionHandler {
private final static Logger LOGGER = LoggerFactory.getLogger(MyHandler.class);
@Override
public void uncaughtException(Thread t, Throwable e) {
LOGGER.error("threadId = {}, threadName = {}, ex = {}", t.getId(), t.getName(), e.getMessage());
}
}
3 直接重写afterExecute()方法,感知异常细节
总结
这篇文章到这里就结束了,不知道小伙伴们有没有一些感悟或收获?
通过这几个面试问题,我也深刻的感受到学习知识要多思考,看源码的过程中要多设置一些场景,这样才会收获更多。
原创干货分享.png
相关推荐
- 高性能并发队列Disruptor使用详解
-
基本概念Disruptor是一个高性能的异步处理框架,是一个轻量的Java消息服务JMS,能够在无锁的情况下实现队列的并发操作Disruptor使用环形数组实现了类似队列的功能,并且是一个有界队列....
- Disruptor一个高性能队列_java高性能队列
-
Disruptor一个高性能队列前言说到队列比较熟悉的可能是ArrayBlockingQueue、LinkedBlockingQueue这两个有界队列,大多应用在线程池中使用能保证线程安全,但其安全性...
- 谈谈防御性编程_防御性策略
-
防御性编程对于程序员来说是一种良好的代码习惯,是为了保护自己的程序在不可未知的异常下,避免带来更大的破坏性崩溃,使得程序在错误发生时,依然能够云淡风轻的处理,但很多程序员入行很多年,写出的代码依然都是...
- 有人敲门,开水开了,电话响了,孩子哭了,你先顾谁?
-
前言哎呀,这种情况你肯定遇到过吧!正在家里忙活着,突然——咚咚咚有人敲门,咕噜咕噜开水开了,铃铃铃电话响了,哇哇哇孩子又哭了...我去,四件事一起来,人都懵了!你说先搞哪个?其实这跟我们写Java多线...
- 面试官:线程池如何按照core、max、queue的执行顺序去执行?
-
前言这是一个真实的面试题。前几天一个朋友在群里分享了他刚刚面试候选者时问的问题:"线程池如何按照core、max、queue的执行循序去执行?"。我们都知道线程池中代码执行顺序是:co...
- 深入剖析 Java 中线程池的多种实现方式
-
在当今高度并发的互联网软件开发领域,高效地管理和利用线程资源是提升程序性能的关键。Java作为一种广泛应用于后端开发的编程语言,为我们提供了丰富的线程池实现方式。今天,就让我们深入探讨Java中...
- 并发编程之《彻底搞懂Java线程》_java多线程并发解决方案详解
-
目录引言一、核心概念:线程是什么?...
- Redis怎么实现延时消息_redis实现延时任务
-
一句话总结Redis可通过有序集合(ZSET)实现延时消息:将消息作为value,到期时间戳作为score存入ZSET。消费者轮询用ZRANGEBYSCORE获取到期消息,配合Lua脚本保证原子性获取...
- CompletableFuture真的用对了吗?盘点它最容易被误用的5个场景
-
在Java并发编程中,CompletableFuture是处理异步任务的利器,但不少开发者在使用时踩过这些坑——线上服务突然雪崩、异常悄无声息消失、接口响应时间翻倍……本文结合真实案例,拆解5个最容易...
- 接口性能优化技巧,有点硬_接口性能瓶颈
-
背景我负责的系统到2021年初完成了功能上的建设,开始进入到推广阶段。随着推广的逐步深入,收到了很多好评的同时也收到了很多对性能的吐槽。刚刚收到吐槽的时候,我们的心情是这样的:...
- 禁止使用这5个Java类,每一个背后都有一段"血泪史"
-
某电商平台的支付系统突然报警:大量订单状态异常。排查日志发现,同一笔订单被重复支付了三次。事后复盘显示,罪魁祸首竟是一行看似无害的SimpleDateFormat代码。在Java开发中,这类因使用不安...
- 无锁队列Disruptor原理解析_无锁队列实现原理
-
队列比较队列...
- Java并发队列与容器_java 并发队列
-
【前言:无论是大数据从业人员还是Java从业人员,掌握Java高并发和多线程是必备技能之一。本文主要阐述Java并发包下的阻塞队列和并发容器,其实研读过大数据相关技术如Spark、Storm等源码的,...
- 线程池工具及拒绝策略的使用_线程池处理策略
-
线程池的拒绝策略若线程池中的核心线程数被用完且阻塞队列已排满,则此时线程池的资源已耗尽,线程池将没有足够的线程资源执行新的任务。为了保证操作系统的安全,线程池将通过拒绝策略处理新添加的线程任务。...
- 【面试题精讲】ArrayBlockingQueue 和 LinkedBlockingQueue 区别?
-
有的时候博客内容会有变动,首发博客是最新的,其他博客地址可能会未同步,认准...
- 一周热门
-
-
C# 13 和 .NET 9 全知道 :13 使用 ASP.NET Core 构建网站 (1)
-
程序员的开源月刊《HelloGitHub》第 71 期
-
详细介绍一下Redis的Watch机制,可以利用Watch机制来做什么?
-
假如有100W个用户抢一张票,除了负载均衡办法,怎么支持高并发?
-
如何将AI助手接入微信(打开ai手机助手)
-
Java面试必考问题:什么是乐观锁与悲观锁
-
SparkSQL——DataFrame的创建与使用
-
redission YYDS spring boot redission 使用
-
一文带你了解Redis与Memcached? redis与memcached的区别
-
如何利用Redis进行事务处理呢? 如何利用redis进行事务处理呢英文
-
- 最近发表
- 标签列表
-
- git pull (33)
- git fetch (35)
- mysql insert (35)
- mysql distinct (37)
- concat_ws (36)
- java continue (36)
- jenkins官网 (37)
- mysql 子查询 (37)
- python元组 (33)
- mybatis 分页 (35)
- vba split (37)
- redis watch (34)
- python list sort (37)
- nvarchar2 (34)
- mysql not null (36)
- hmset (35)
- python telnet (35)
- python readlines() 方法 (36)
- munmap (35)
- docker network create (35)
- redis 集合 (37)
- python sftp (37)
- setpriority (34)
- c语言 switch (34)
- git commit (34)