在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如滴滴、极兔、有赞、希音、百度、网易的面试资格,遇到很多很重要的面试题:
- 如何设计线程池?
- 请手写一个简单线程池?
就在昨天, 一个小伙伴面试滴滴, 遇到一个与线程池底层原理有关的连环炮, 没有回答好,导致面试挂了。
小伙伴遇到的滴滴的面试真题,也就这个与线程池底层原理有关的连环炮,用小伙的原话来说吧。
小伙伴的原话如下:
恩哥,最近滴滴一面遇到一个问题,问的是
尼恩提示:线程池的知识,既是面试的核心知识,又是开发的核心知识。 所以,这里尼恩给大家做一下系统化、体系化的线程池梳理,使得大家可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”。
也一并把这个题目以及参考答案,收入咱们的 《尼恩Java面试宝典PDF》V110版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请关注本公众号【技术自由圈】取
第一问:线程池底部是怎么进行线程调度的?
这个是一个基础题,具体的答案,请参考尼恩的 《Java 高并发核心编程 卷1 加强版》
第二问:线程如何进行抢占和优先级设置?
这个需要大家了解一下线程池的运作原理, 最好是自己手写一个简单的线程池,加深印象。
如何手写一个线程池呢? 请参考尼恩 架构团队的文章
网易一面:如何设计线程池?请手写一个简单线程池?
第三问:对于有优先级要求的场景下,怎么设置线程池?
这个,使用本文给大家作答。
一般而言,大家都使用线程池并发执行、并发调度任务,通过池化的架构去节省线程创建和销毁带来的性能损耗。
默认情况下,有了线程池之后,大家都通过提交任务的方式, 提交任务到线程池,由线程池去调度。
提交到线程池的任务,按照线程池的调度规则进行调度。线程池的调度规则,大致如下:
注意:请点击图像以查看清晰的视图!
如何线程池的核心线程都很忙,任务就需要排队了,进入线程池的内部工作队列,大致如下图所示:
注意:请点击图像以查看清晰的视图!
工作线程执行完手上的任务后,会在一个无限循环中,反复从内部工作队列(如LinkedBlockingQueue )获取任务来执行。
普通的线程池, 任务之间是没有优先级特权的, 可以理解为 先进先出 的公平调度模式。
有的的时候, 任务之间是有 优先级特权的, 不是按照 先进先出 调度, 而是需要按照 优先级进行调度。
所以,如果不同的任务之间,存在一些优先级的变化,咋整呢?
办法很简单,就是替换掉 线程池里边的工作队列,使用 优先级的无界阻塞队列 ,去管理 异步任务。
首先,来看看几种典型的工作队列
使用 优先级的无界阻塞队列 PriorityBlockingQueue 替代 没有优先级的队列 ArrayBlockingQueue 或者 LinkedBlockingQueue。
额外提一嘴, 如果是普通的任务,没有优先级的话, 一般情况,建议大家参考 rocketmq的源码, 使用 有界的 LinkedBlockingQueue 作为 任务队列。
rocketmq的源码, 用到大量的线程池,具体如下图:
这些任务队列,用的都是有界的 LinkedBlockingQueue ,具体如下图:
如果任务有优先级, 就需要引入任务队列并进行管理了。
这时候,就需要 使用 优先级的无界阻塞队列 PriorityBlockingQueue ,下面是一个例子。
实现一个 优先级任务线程池,有2个关键点:
还是基于ThreadPoolExecutor 进行线程池的构造, 我们知道, ThreadPoolExecutor的构造函数有一个workQueue参数,这里可以传入 PriorityBlockingQueue 优先级队列。
在 buildWorkQueue() 方法里边,构造一个 PriorityBlockingQueue
,它的构造函数可以传入一个比较器Comparator,能够满足要求。
这里主要有两点:
Comparable
才能进行比较。ThreadPoolExecutor的submit、invokeXxx、execute方法入参都是Runnable、Callable,均不具备可排序的属性。
我们可以弄一个实现类 PriorityTask,加一些额外的属性,让它们具备排序能力。
提交三种不同优先级的任务,进行测试
优先级高的前面执行
优先级低的后面执行
主要是,PriorityBlockingQueue是无界的,它的offer方法永远返回true。
这样,会带来两个问题:
第一,OOM风险;
第二,最大线程数 失效
第三,拒绝策略 失效
怎么解决呢?
方法一:可以继承PriorityBlockingQueue , 重写一下这个类的offer方法,如果元素超过指定数量直接返回false,否则调用原来逻辑。
方式二:扩展线程池的submit、invokeXxx、execute方法,在里边进行 任务数量的 统计、检查、限制。
优先建议大家使用 方式一。
面试进行到这里,很容易出现 PriorityQueue的堆结构的问题
因为, PriorityBlockingQueue 是 PriorityQueue 的阻塞版本
PriorityQueue在默认情况下是一个最小堆,如果使用最大堆调用构造函数就需要传入 Comparator
改变比较排序的规则。
// 构造小顶堆
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>((o1, o2) -> o1 - o2);
// 构造大顶堆
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>((o1, o2) -> o2 - o1);
PriorityQueue
实现了接口 Queue
,它常用的函数如表所示
这就是为啥堆被称为优先级队列的原因
操作 | 抛异常 | 不抛异常 |
---|---|---|
插入新的元素 | add(e) | offer(e) |
删除堆顶元素 | remove | poll |
返回堆顶元素 | element | peek |
虽然Java中的PriorityQueue实现了Queue接口,但它并不是一个队列,也不是按照“先入先出”的顺序删除元素的。
PriorityQueue的删除顺序与元素添加的顺序无关。PriorityQueue是 按照最小堆 的 次序,进行元素操作的。
所以,PriorityQueue是一个堆,每次调用函数remove或poll都将删除位于堆顶的元素。
同理,PriorityQueue的函数element和peek都返回位于堆顶的元素,即根据堆的类型返回值最大或最小的元素,这与元素添加的顺序无关。
堆(也称为优先级队列)是一种特殊的树形数据结构。
根据根节点的值与子节点的值的大小关系,堆又分为最大堆和最小堆。
例如,图(a)所示是一个最大堆,图(b)所示是一个最小堆。
堆通常用完全二叉树实现。在完全二叉树中,除最低层之外,其他层都被节点填满,最低层尽可能从左到右插入节点。上图中的两个堆都是完全二叉树。
完全二叉树又可以用数组实现,因此堆也可以用数组实现。如果从堆的根节点开始从上到下按层遍历,并且每层从左到右将每个节点按照 0、1、2 等的顺序编号,将编号为 0 的节点放入数组中下标为 0 的位置,编号为1的节点放入数组中下标为1的位置,以此类推就可以将堆的所有节点都添加到数组中。上图(a)中的堆可以用数组表示成下图(a)所示的形式,而上图(b)中的堆可以用数组表示成下图(b)所示的形式。
如果数组中的一个元素的下标为 i,那么它在堆中对应节点的父节点在数组中的下标为 (i - 1) / 2
,而它的左右子节点在数组中的下标分别为 2 * i + 1
和 2 * i + 2
。
为了在最大堆中添加新的节点,有以下三个步骤:
在最小堆中添加新节点的过程与此类似,唯一的不同是要确保新节点的值要大于或等于它的父节点。
所以,堆的添加操作是一个自下而上的操作。
举个例子,如果上图(a)的最大堆中添加一个新的元素 95:
整体过程如下图:
堆的插入操作可能需要交换节点,以便把节点放到合适的位置,交换的次数最多为二叉树的深度,因此如果堆中有 n 个节点,那么它的插入操作的时间复杂度是 O(logn)
。
通常只删除位于堆顶部的元素。
以删除最大堆的顶部节点为例:
删除最小堆的顶部节点的过程与此类似,唯一的不同是要确保节点的值要小于它的左右子节点的值。
所以,堆的删除操作是一个自上而下的操作。
举个例子,删除上图(a)中最大堆的顶部元素之后:
堆的删除操作可能需要交换节点,以便把节点放到合适的位置,交换的次数最多为二叉树的深度,因此如果堆中有 n 个节点,那么它的删除操作的时间复杂度是 O(logn)
。
线程池面试题,是非常常见的面试题。
以上的内容,如果大家能对答如流,如数家珍,基本上 面试官会被你 震惊到、吸引到。
在面试之前,建议大家系统化的刷一波 5000页《尼恩Java面试宝典PDF》,并且在刷题过程中,如果有啥问题,大家可以来 找 40岁老架构师尼恩交流。
最终,让面试官爱到 “不能自已、口水直流”。offer, 也就来了。
《百亿级访问量,如何做缓存架构设计》
《多级缓存 架构设计》
《消息推送 架构设计》
《阿里2面:你们部署多少节点?1000W并发,当如何部署?》
《美团2面:5个9高可用99.999%,如何实现?》
《网易一面:单节点2000Wtps,Kafka怎么做的?》
《字节一面:事务补偿和事务重试,关系是什么?》
《网易一面:25Wqps高吞吐写Mysql,100W数据4秒写完,如何实现?》
《亿级短视频,如何架构?》
《炸裂,靠“吹牛”过京东一面,月薪40K》
《太猛了,靠“吹牛”过顺丰一面,月薪30K》
《炸裂了…京东一面索命40问,过了就50W+》
《问麻了…阿里一面索命27问,过了就60W+》
《百度狂问3小时,大厂offer到手,小伙真狠!》
《饿了么太狠:面个高级Java,抖这多硬活、狠活》
《字节狂问一小时,小伙offer到手,太狠了!》
《收个滴滴Offer:从小伙三面经历,看看需要学点啥?》
《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》PDF,请到下面公号【技术自由圈】取↓↓↓