关于面试技巧:Java线程池面试要点

3次阅读

共计 5211 个字符,预计需要花费 14 分钟才能阅读完成。

作者:August Rush
起源:淘系技术

Java 线程池在面试的时候问的挺多的,已经我就在面试过程中两次被问到,面试官通过面试者对线程池的了解答复也能大抵理解到面试者的理论开发教训如何,以及对多线程的了解使用有没有深刻到位。

同时,面试官在切入多线程问题的时候通常也不会太过僵硬,而是一步一步通过线程创立形式、线程状态切换、线程协同疏导过去,整体谈下来其实也挺花工夫的,会涉及到多线程的方方面面,但对开发者素质的确也是一番不小的考验,明天咱们也不齐全铺开去形容,就仅仅针对线程池这一点来聊聊面试的时候会碰到的一些问题。

ThreadPoolExecutor 参数含意

ThreadPoolExecutor 结构函数参数定义咱们能够间接在 concurrent 包当中找到。

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {}

这几个外围参数的含意别离是:

  • corePoolSize:线程池外围线程数量,外围线程不会被回收,即便没有工作执行,也会放弃闲暇状态,设置 allowCoreThreadTimeOut 参数为 true 才会进行回收。如果线程池中的线程少于此数目,则在执行工作时创立。
  • maximumPoolSize:线程池最大线程数,示意在线程池中最多能创立多少个线程。当线程数量达到 corePoolSize,且 workQueue 队列塞满工作了之后,持续创立线程,当线程池中的线程数量达到这个数字时,新来的工作会执行回绝策略。
  • keepAliveTime:示意线程没有工作执行时最多能放弃多少工夫会被回收,留神,这个参数管制的是超过 corePoolSize 之后的“长期线程”的存活工夫。
  • unit:参数 keepAliveTime 的工夫单位。
  • workQueue:工作队列,寄存提交的期待工作,其中有队列大小的限度。
  • threadFactory:创立线程的工厂类,通常咱们会自定义一个 threadFactory 设置线程的名称,这样咱们就能够晓得线程是由哪个工厂类创立的,能够疾速定位排查问题。
  • handler:如果线程池已满,新的工作进来时的回绝策略。

ThreadPoolExecutor 参数含意是最常见的一个问题,如果面试者对这些参数比拟理解,至多阐明面试者在多线程使用层面不会存在太大的问题,反之,如果面试官提醒某个参数后面试者还是一脸懵的话,那么根底印象分就会大打折扣。

线程池线程创立的流程是怎么的

线程池线程创立的机会能够用上面这张图简略示意。

线程创立流程是这样的:

  • 如果以后运行的线程少于 corePoolSize(外围线程数),则创立新线程来执行工作(执行这一步骤须要获取全局锁)。
  • 如果运行的线程等于或多于 corePoolSize,则将工作退出 BlockingQueue(阻塞队列 / 工作队列)。
  • 如果无奈将工作退出 BlockingQueue(队列已满),则在非 corePool 中创立新的线程来解决工作(执行这一步骤也须要获取全局锁)。
  • 如果创立新线程将使得以后运行的线程超出 maximumPoolSize 限度,工作将被回绝,并执行线程饱和策略,如:RejectedExecutionHandler.rejectedExecution()办法。

留神:初始化线程池时,线程数为 0。

工作列队有哪几种实现

寄存工作的工作队列有 6 种次要的实现,别离是 ArrayBlockingQueue、LinkedBlockingQueue、LinkedBlockingDeque、PriorityBlockingQueue、DelayQueue、SynchronousQueue。它们的区别如下:

  • ArrayBlockingQueue:一个由数组构造组成的有界阻塞队列(数组构造可配合指针实现一个环形队列)。
  • LinkedBlockingQueue:一个由链表构造组成的有界阻塞队列,在未指明容量时,容量默认为 Integer.MAX_VALUE。
  • LinkedBlockingDeque:应用双向队列实现的双端阻塞队列,双端意味着能够像一般队列一样 FIFO(先进先出),能够以像栈一样 FILO(先进后出)
  • PriorityBlockingQueue:一个反对优先级排序的无界阻塞队列,对元素没有要求,能够实现 Comparable 接口也能够提供 Comparator 来对队列中的元素进行比拟,跟工夫没有任何关系,仅仅是依照优先级取工作。
  • DelayQueue:同 PriorityBlockingQueue,也是二叉堆实现的优先级阻塞队列。要求元素都实现 Delayed 接口,通过执行时延从队列中提取工作,工夫没到工作取不进去。
  • SynchronousQueue:一个不存储元素的阻塞队列,消费者线程调用 take() 办法的时候就会产生阻塞,直到有一个生产者线程生产了一个元素,消费者线程就能够拿到这个元素并返回;生产者线程调用 put()办法的时候就会产生阻塞,直到有一个消费者线程生产了一个元素,生产者才会返回。

回绝策略有哪几种

线程池中的线程曾经用完了,无奈持续为新工作服务,同时,期待队列也曾经排满了,再也塞不下新工作了。这时候咱们就须要回绝策略机制正当地解决新进来的工作。JDK 内置的四种回绝策略如下:

  • AbortPolicy(默认):抛弃工作并抛出 RejectedExecutionException 异样。
  • CallerRunsPolicy:由调用线程解决该工作。(例如 io 操作,线程生产速度没有 NIO 快,可能导致阻塞队列始终减少,此时能够应用这个模式)。
  • DiscardPolicy:抛弃工作,然而不抛出异样。(能够配合这种模式进行自定义的解决形式)。
  • DiscardOldestPolicy:抛弃队列最早的未解决工作,而后从新尝试执行工作。

线程池的分类

Java 外面线程池的顶级接口是 Executor,然而严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具,真正的线程池接口是 ExecutorService。Java 中 Executors 工厂类能够为咱们主动创立不同策略配置的线程池,供咱们间接应用。

▐ newCachedThreadPool

coreSize 线程数 0,最大线程数无限度,线程的容许闲暇工夫是 60s,阻塞队列是 SynchronousQueue。实用于“短工作”状况。因为采纳 SynchronousQueue,每当提交一个工作,都会超过阻塞队列的长度,导致创立新线程解决,所以说:每当提交一个工作,都会创立一个线程,可能造成 OOM。此外,线程闲暇 1 分钟就会销毁,所以该线程池可能会频繁地创立和销毁线程。

▐ newFixedThreadPool

coreSize 和最大线程数都是用户输出的,阻塞队列用的 LinkedBlockingQueue,线程的容许闲暇工夫是 0s。其外围个性就是线程数不会减少,不会缩小,线程池也不会本人销毁。因为阻塞队列是无限大的,不会执行回绝策略。所以可能会沉积有限的申请,导致 OOM。

▐ newSingleThreadExecutor

相当于线程数为 1 的 newFixedThreadPool,毛病和 newFixedThreadPool 一样。有的小伙伴可能会问,那它和单个线程有什么区别?

newSingleThreadExecutor Thread
工作执行实现后,不会主动销毁,能够复用 工作执行实现后,会主动销毁
能够将工作存储在阻塞队列中,一一执行 无奈存储工作,只能执行一个工作

▐ newScheduledThreadPool

反对定时及周期性工作执行,须要留神的是,如果工作执行过程中抛出了异样就会进行执行工作,而且也不会再周期地执行该工作了。所以如果想放弃工作周期执行,须要 catch 所有可能的异样。

▐ newWorkStealingPool

采纳的 ForkJoin 框架,能够将工作进行宰割,同时线程之间会互相帮助。另外,阻塞队列采纳的 LinkedBlockingDeque,能够进行工作窃取。因为理论应用不多,这里只作理解。

理论应用时并不举荐这样去间接创立应用,阿里 Java 开发规约外面也有相应束缚:

【强制】线程池不容许应用 Executors 去创立,而是通过 ThreadPoolExecutor 的形式,这样的解决形式让写的同学更加明确线程池的运行规定,躲避资源耗尽的危险。阐明:Executors 返回的线程池对象的弊病如下:

1)FixedThreadPool 和 SingleThreadPool: 容许的申请队列长度为 Integer.MAX_VALUE,可能会沉积大量的申请,从而导致 OOM。

2)CachedThreadPool 和 ScheduledThreadPool: 容许的创立线程数量为 Integer.MAX_VALUE,可能会创立大量的线程,从而导致 OOM。

如何敞开线程池

▐ shutdown(高平安低响应)

实质上执行的是 interrupt 办法,阻止新来的工作提交,会将线程池的状态改成 SHUTDOWN,当再执行 execute 提交工作时,如果测试到状态不为 RUNNING,则执行回绝策略,从而达到阻止新工作提交的目标。对于曾经提交的工作不会产生任何影响,当曾经提交的工作执行完当前,它会将那些闲置的线程进行中断,这个过程是异步的,也就是说只会打断闲暇线程,如果以后还有工作队列还有工作未执行,线程将持续把工作执行完。

▐ shutdownNow(低平安高响应)

将阻止新来的工作提交,同时将线程池的状态改成 STOP,当再执行 execute 提交工作时,如果测试到状态不为 RUNNING,则抛出 rejectedExecution,从而达到阻止新工作提交的目标。该办法会中断闲暇过程,同时也会中断以后正在运行的线程,即 workers 中的线程。如果遇到曾经激活的工作,并且处于阻塞状态时,shutdownNow() 会执行 1 次中断阻塞的操作,此时对应的线程报 InterruptedException,如果后续还要期待某个资源,则按失常逻辑期待某个资源的达到。例如,一个线程正在 sleep 状态中,此时执行 shutdownNow(),它向该线程发动 interrupt() 申请,而 sleep() 办法遇到有 interrupt() 申请时,会抛出 InterruptedException(),并持续往下执行。在这里要揭示留神的是,在激活的工作中,如果有多个 sleep(), 该办法只会中断第一个 sleep(),而前面的依然依照失常的执行逻辑进行。

两张敞开线程池的形式的次要区别用一句话概括就是:高平安低响 应体现在 shutdown 期待工作执行实现再敞开,能够保障工作肯定被执行,然而敞开线程池须要期待较长的工夫;低平安高响应 体现在 shutdownNow 会敞开正在执行工作的线程,工作可能并没有执行结束,也不会回退到工作队列中,将会隐没,然而敞开线程池不须要期待较长的工夫。

线程池外围线程数教训配置

CPU 密集型工作:尽量压迫 CPU,参考值设置为 CPU 的个数 +1。
IO 密集型工作:参考值能够设置为 CPU 的个数 ✖️ 2。

以上只是教训配置参考,具体的应用配置如果在条件容许的状况下最好应用公司的压测工具或环境压测一下。

应用线程池有什么益处

  • 线程重用:线程的创立和销毁开销是微小的,而通过线程池的重用大大减少了这些不必要的开销,当然既然少了这么多开销,其线程执行速度也是突飞猛进的晋升。
  • 控制线程池的并发数:线程不是并发的越多,性能越高,反而在线程并发太多时,线程的切换会耗费零碎大量的资源,能够通过设置线程池最大并发线程数目,维持零碎高性能。
  • 线程池能够对线程进行治理:尽管线程提供了线程组操控线程,然而线程池领有更多治理线程的 API。
  • 能够贮存须要执行的工作:当工作提交过多时,能够将工作储存起来,期待线程解决。

最初

以上就是对于线程池的一些外围要点了,单从应用的角度来说,有些细节不必理解的太深刻,看完也就忘了。但从面试的角度来说还是须要尽量理解全面一些,至多得对得起简历上那句“对技术有谋求”不是?对于线程池明天就介绍这么多,有其余脱漏须要补充的欢送留言探讨。

我整顿了一些学习材料,外面包含 Java 根底、Android 进阶、架构设计、NDK、音视频开发、跨平台、底层源码等技术,还有 2022 年一线大厂最新面试题集锦,都分享给大家,助大家学习路上乘风破浪~ 能力失去晋升,思维失去宽阔~ 有须要的能够 点击下方链接收费获取

链接:https://shimo.im/docs/R13j85m…

正文完
 0