美团二面拷打:如何设计一个优先级任务线程池?
The following article is from JavaGuide Author Guide
本文经JavaGuide(id:JavaGuide)授权转载
如若转载请联系原公众号
这是一位球友最近面试遇到的一个问题,由于没能回答出来导致面试失败。这个问题挺常见的,本质其实还是在考察求职者对于线程池以及阻塞队列的掌握,也不是说非要让你设计出一个非常牛逼的优先级任务线程池。
开始之前,我们先来回顾一下线程池处理任务的流程。
如果当前运行的线程数小于核心线程数,那么就会新建一个线程来执行任务。 如果当前运行的线程数等于或大于核心线程数,但是小于最大线程数,那么就把该任务放入到任务队列里等待执行。 如果向任务队列投放任务失败(任务队列已经满了),但是当前运行的线程数是小于最大线程数的,就新建一个线程来执行任务。 如果当前运行的线程数已经等同于最大线程数了,新建线程将会使当前运行的线程超出最大线程数,那么当前任务会被拒绝,饱和策略会调用 RejectedExecutionHandler.rejectedExecution()
方法。
可以看到,新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
不同的线程池会选用不同的阻塞队列,我们可以结合内置线程池来分析。
容量为 Integer.MAX_VALUE
的LinkedBlockingQueue
(无界队列):FixedThreadPool
和SingleThreadExector
。由于队列永远不会被放满,因此FixedThreadPool
最多只能创建核心线程数的线程,SingleThreadExector
只能创建一个线程。SynchronousQueue
(同步队列):CachedThreadPool
。SynchronousQueue
没有容量,不存储元素,目的是保证对于提交的任务,如果有空闲线程,则使用空闲线程来处理;否则新建一个线程来处理任务。也就是说,CachedThreadPool
的最大线程数是Integer.MAX_VALUE
,可以理解为线程数是可以无限扩展的,可能会创建大量线程,从而导致 OOM。DelayedWorkQueue
(延迟阻塞队列):ScheduledThreadPool
和SingleThreadScheduledExecutor
。DelayedWorkQueue
的内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构,可以保证每次出队的任务都是当前队列中执行时间最靠前的。DelayedWorkQueue
添加元素满了之后会自动扩容原来容量的 1/2,即永远不会阻塞,最大扩容可达Integer.MAX_VALUE
,所以最多只能创建核心线程数的线程。
假如我们需要实现一个优先级任务线程池的话,那可以考虑使用 PriorityBlockingQueue
(优先级阻塞队列)作为任务队列(ThreadPoolExecutor
的构造函数有一个 workQueue
参数可以传入任务队列)。
PriorityBlockingQueue
是一个支持优先级的无界阻塞队列,可以看作是线程安全的 PriorityQueue
,两者底层都是使用小顶堆形式的二叉堆,即值最小的元素优先出队。不过,PriorityQueue
不支持阻塞操作。
要想让 PriorityBlockingQueue
实现对任务的排序,传入其中的任务必须是具备排序能力的,方式有两种:
提交到线程池的任务实现 Comparable
接口,并重写compareTo
方法来指定任务之间的优先级比较规则。创建 PriorityBlockingQueue
时传入一个Comparator
对象来指定任务之间的排序规则(推荐)。
不过,这存在一些风险和问题,比如:
PriorityBlockingQueue
是无界的,可能堆积大量的请求,从而导致 OOM。可能会导致饥饿问题,即低优先级的任务长时间得不到执行。 由于需要对队列中的元素进行排序操作以及保证线程安全(并发控制采用的是可重入锁 ReentrantLock
),因此会降低性能。
对于 OOM 这个问题的解决比较简单粗暴,就是继承PriorityBlockingQueue
并重写一下 offer
方法(入队)的逻辑,当插入的元素数量超过指定值就返回 false 。
饥饿问题这个可以通过优化设计来解决(比较麻烦),比如等待时间过长的任务会被移除并重新添加到队列中,但是优先级会被提升。
对于性能方面的影响,是没办法避免的,毕竟需要对任务进行排序操作。并且,对于大部分业务场景来说,这点性能影响是可以接受的。
字节二面:DNS 解析一个地址的时候会返回多个 IP 吗? 腾讯二面顶住了!评价反馈不错 面渣逆袭:分布式十二问!万字图文详解! 字节二面:Redis 的大 Key 对持久化有什么影响? 我麻了,京东一面:守护线程如何实现的?