JAVA面试题分享二百七十九:线程池任务,如何设置优先级?

目录

线程池的基本原理

对于有优先级要求的场景下,怎么设置线程池?

优先级任务线程池的设计与实操

使用 PriorityBlockingQueue 作为 线程池的任务队列

提交的任务 具备 排序能力

对自定义的优先级线程池,进行测试

PriorityBlockingQueue 队列的问题

说说 PriorityQueue 的堆结构

PriorityQueue 是 Java 提供的堆实现

来到算法的基础知识,什么是堆?

在堆中添加元素

在堆中删除元素


线程池的基本原理

一般而言,大家都使用线程池并发执行、并发调度任务,通过池化的架构去节省线程创建和销毁带来的性能损耗。

默认情况下,有了线程池之后,大家都通过提交任务的方式, 提交任务到线程池,由线程池去调度。

提交到线程池的任务,按照线程池的调度规则进行调度。线程池的调度规则,大致如下:

注意:请点击图像以查看清晰的视图!

如何线程池的核心线程都很忙,任务就需要排队了,进入线程池的内部工作队列,大致如下图所示:

注意:请点击图像以查看清晰的视图!

工作线程执行完手上的任务后,会在一个无限循环中,反复从内部工作队列(如LinkedBlockingQueue )获取任务来执行。

对于有优先级要求的场景下,怎么设置线程池?

普通的线程池, 任务之间是没有优先级特权的, 可以理解为 先进先出 的公平调度模式。

有的的时候, 任务之间是有 优先级特权的, 不是按照 先进先出 调度, 而是需要按照 优先级进行调度。

所以,如果不同的任务之间,存在一些优先级的变化,咋整呢?

办法很简单,就是替换掉 线程池里边的工作队列,使用 优先级的无界阻塞队列 ,去管理 异步任务。

首先,来看看几种典型的工作队列

  • ArrayBlockingQueue:使用数组实现的有界阻塞队列,特性先进先出

  • LinkedBlockingQueue:使用链表实现的阻塞队列,特性先进先出,可以设置其容量,默认为Interger.MAX_VALUE,特性先进先出

  • PriorityBlockingQueue:使用平衡二叉树堆,实现的具有优先级的无界阻塞队列

  • DelayQueue:无界阻塞延迟队列,队列中每个元素均有过期时间,当从队列获取元素时,只有过期元素才会出队列。队列头元素是最块要过期的元素。

  • SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作,必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态

使用 优先级的无界阻塞队列 PriorityBlockingQueue 替代 没有优先级的队列 ArrayBlockingQueue 或者 LinkedBlockingQueue。

额外提一嘴, 如果是普通的任务,没有优先级的话, 一般情况,建议大家参考 rocketmq的源码, 使用 有界的 LinkedBlockingQueue 作为 任务队列。

rocketmq的源码, 用到大量的线程池,具体如下图:

图片

这些任务队列,用的都是有界的 LinkedBlockingQueue ,具体如下图:

图片

如果任务有优先级, 就需要引入任务队列并进行管理了。

这时候,就需要 使用 优先级的无界阻塞队列 PriorityBlockingQueue ,下面是一个例子。

优先级任务线程池的设计与实操

实现一个 优先级任务线程池,有2个关键点:

  • 使用PriorityBlockingQueue  作为 线程池的任务队列。

  • 提交的任务 具备 排序能力。

使用 PriorityBlockingQueue 作为 线程池的任务队列

还是基于ThreadPoolExecutor 进行线程池的构造, 我们知道, ThreadPoolExecutor的构造函数有一个workQueue参数,这里可以传入 PriorityBlockingQueue   优先级队列。

在 buildWorkQueue() 方法里边,构造一个 PriorityBlockingQueue<E>,它的构造函数可以传入一个比较器Comparator,能够满足要求。

这里主要有两点:

  • 替换线程池默认的阻塞队列为 PriorityBlockingQueue,响应的传入的线程类需要实现 Comparable<T> 才能进行比较。

  • PriorityBlockingQueue 的数据结构决定了,优先级相同的任务无法保证 FIFO,需要自己控制顺序。

提交的任务 具备 排序能力

ThreadPoolExecutor的submit、invokeXxx、execute方法入参都是Runnable、Callable,均不具备可排序的属性。

我们可以弄一个实现类 PriorityTask,加一些额外的属性,让它们具备排序能力。

对自定义的优先级线程池,进行测试

提交三种不同优先级的任务,进行测试

  • 优先级高的后面提交

  • 优先级低的前面提交

优先级高的前面执行

图片

优先级低的后面执行

图片

PriorityBlockingQueue 队列的问题

主要是,PriorityBlockingQueue是无界的,它的offer方法永远返回true。

这样,会带来两个问题:

第一,OOM风险

第二,最大线程数 失效

第三,拒绝策略 失效

怎么解决呢?

方法一:可以继承PriorityBlockingQueue , 重写一下这个类的offer方法,如果元素超过指定数量直接返回false,否则调用原来逻辑。

方式二:扩展线程池的submit、invokeXxx、execute方法,在里边进行 任务数量的 统计、检查、限制。

优先建议大家使用 方式一。

说说 PriorityQueue 的堆结构

面试进行到这里,很容易出现 PriorityQueue的堆结构的问题

因为, PriorityBlockingQueue  是 PriorityQueue 的阻塞版本

PriorityQueue 是 Java 提供的堆实现

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)
删除堆顶元素removepoll
返回堆顶元素elementpeek

虽然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

在堆中添加元素

为了在最大堆中添加新的节点,有以下三个步骤:

  1. 先从上到下、从左到右找出第 1 个空缺的位置,并将新节点添加到该空缺位置

  2. 如果新节点的值比它的父节点的值大,那么交换它和它的父节点

  3. 重复这个交换过程,直到新节点的值小于或等于它的父节点,或者它已经到达堆的顶部位置。

在最小堆中添加新节点的过程与此类似,唯一的不同是要确保新节点的值要大于或等于它的父节点。

所以,堆的添加操作是一个自下而上的操作。

举个例子,如果上图(a)的最大堆中添加一个新的元素 95:

  • 由于节点 60 的右子节点是第1个空缺的位置,因此创建一个新的节点95并使之成为节点60的右子节点

  • 此时新节点95的值大于它的父节点60的值,这违背了最大堆的定义,于是交换它和它的父节点

  • 由于新节点95的值仍然大于它的父节点90的值,因此再交换新节点95和它的父节点90。此时堆已经满足最大堆的定义。

整体过程如下图:

堆的插入操作可能需要交换节点,以便把节点放到合适的位置,交换的次数最多为二叉树的深度,因此如果堆中有 n 个节点,那么它的插入操作的时间复杂度是 O(logn)

在堆中删除元素

通常只删除位于堆顶部的元素

以删除最大堆的顶部节点为例:

  1. 将堆最低层最右边的节点移到堆的顶部

  2. 如果此时它的左子节点或右子节点的值大于它,那么它和左右子节点中值较大的节点交换

  3. 如果交换之后节点的值仍然小于它的子节点的值,则再次交换,直到该节点的值大于或等于它的左右子节点的值,或者到达最低层为止

删除最小堆的顶部节点的过程与此类似,唯一的不同是要确保节点的值要小于它的左右子节点的值。

所以,堆的删除操作是一个自上而下的操作。

举个例子,删除上图(a)中最大堆的顶部元素之后:

  1. 将位于最低层最右边的节点60移到最大堆的顶部,如下图(c)所示

  2. 此时节点60比它的左子节点80和右子节点90的值都小,因此将它和值较大的右子节点90交换,交换之后的堆如图(d)所示

  3. 此时节点60大于它的左子节点30,满足最大堆的定义

堆的删除操作可能需要交换节点,以便把节点放到合适的位置,交换的次数最多为二叉树的深度,因此如果堆中有 n 个节点,那么它的删除操作的时间复杂度是 O(logn)

  • 21
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
线程池Java中用于管理和复用线程的机制,它可以提高线程的利用率和性能。线程池的七大参数包括: 1. corePoolSize(核心线程数):线程池中始终保持的线程数量,即使它们处于空闲状态。当任务数量超过核心线程数时,线程池会创建新的线程来处理任务。 2. maximumPoolSize(最大线程数):线程池中允许存在的最大线程数量。当任务数量超过最大线程数时,新的任务会被放入等待队列中等待执行。 3. keepAliveTime(线程空闲时间):当线程池中的线程数量超过核心线程数时,多余的空闲线程在等待新任务到来时的最长等待时间。超过这个时间,空闲线程会被销毁。 4. unit(时间单位):用于设置keepAliveTime的时间单位,可以是秒、毫秒、分钟等。 5. workQueue(任务队列):用于存放等待执行的任务的队列。常见的队列类型有有界队列(如ArrayBlockingQueue)和无界队列(如LinkedBlockingQueue)。 6. threadFactory(线程工厂):用于创建新线程的工厂类。可以自定义线程的名称、优先级等属性。 7. handler(拒绝策略):当任务无法被线程池执行时的处理策略。常见的策略有直接抛出异常、丢弃任务、丢弃队列中最旧的任务等。 线程池的作用主要有以下几点: 1. 提高性能:线程池可以复用线程,避免了频繁创建和销毁线程的开销,提高了系统的性能。 2. 控制资源:通过设置核心线程数和最大线程数,可以控制系统中并发线程的数量,避免资源被过度占用。 3. 提供任务队列:线程池可以提供一个任务队列,用于存放等待执行的任务。当线程池中的线程都在执行任务时,新的任务会被放入队列中等待执行。 4. 管理线程:线程池可以统一管理线程的生命周期,包括创建、销毁、空闲时间等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

之乎者也·

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值