面向面试编程:并发编程中的线程池

117 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第2天,点击查看活动详情

面试官:看你项目中用到了线程池,说说你了解的线程池

JDK自带的线程池

JDK中基于Executors提供了很多种线程池。

newFixedThreadPool

这个线程池的特别是线程数是固定的。构建时,需要给newFixedThreadPool方法提供一个nThreads的属性,而这个属性其实就是当前线程池中线程的个数。当前线程池的本质其实就是使用ThreadPoolExecutor。

newSingleThreadExecutor

看名字就知道是单例线程池,线程池中只有一个工作线程在处理任务。如果业务涉及到顺序消费,可以采用newSingleThreadExecutor。

newCachedThreadPool

当第一次提交任务到线程池时,会直接构建一个工作线程。这个工作线程带执行完任务,60秒没有任务可以执行后,会结束。如果在等待60秒期间有任务进来,他会再次拿到这个任务去执行。如果后续提升任务时,没有线程是空闲的,那么就构建工作线程去执行。最大的一个特点,任务只要提交到当前的newCachedThreadPool中,就必然有工作线程可以处理,最大线程数是Integer.MAX_VALUE。

newScheduleThreadPool

这是一个定时任务的线程池,这个线程池是可以以一定周期去执行一个任务,或者是延迟多久执行一个任务一次。构建的是ScheduledThreadPoolExecutor线程池,所以本质上还是正常线程池,只不过在原来的线程池基础上实现了定时任务的功能。原理是基于DelayQueue实现的延迟执行。周期性执行是任务执行完毕后,再次扔回到阻塞队列。

newWorkStealingPool

newWorkStealingPool是基于ForkJoinPool构建出来的。ForkJoin的一个特点是可以将一个大任务拆分成多个小任务,放到当前线程的阻塞队列中。其他的空闲线程就可以去处理有任务的线程的阻塞队列中的任务,从而实现分而治之的处理方式。

自定义线程池

Executors中的构建线程池的方式,大多数还是基于ThreadPoolExecutor去new出来的。

ThreadPoolExecutor中,一共提供了7个参数,每个参数都是非常核心的属性,在线程池去执行任务时,每个参数都有决定性的作用。

如果直接采用JDK提供的方式去构建,可以设置的核心参数最多就两个,这样就会导致对线程池的控制粒度很粗。所以在阿里规范中也推荐自己去自定义线程池。手动的去new ThreadPoolExecutor设置他的一些核心属性。

自定义构建线程池,可以细粒度的控制线程池,去管理内存的属性,并且针对一些参数的设置可能更好的在后期排查问题。

线程池创建参数

面试官:既然说到自定义线程池,那你说说线程池的参数有哪些。
  • corePoolSize(核心线程数):当前任务执行结束后,不会被销毁。
  • maximumPoolSize(最大线程数):代表当前线程池中,一共可以有多少个工作线程,使用无界队列时设置该参数无效。
  • workQueue(工作队列):任务在没有核心工作线程处理时,先扔到阻塞队列中。
  • keepAliveTime(线程活动保持时间):非核心工作线程在阻塞队列位置等待的时间。
  • unit(线程活动保持时间的单位):非核心工作线程在阻塞队列位置等待时间的单位。
  • handler(饱和策略,或者又称拒绝策略):当线程池无法处理投递过来的任务时,执行当前的拒绝策略。
  • threadFactory(构建线程的工厂类):构建线程的线程工厂,可以设置thread的一些信息。

拒绝策略

面试官:能否说说你常用的拒绝策略?
  • AbortPolicy:当前拒绝策略会在无法处理任务时,直接抛出一个异常。
  • CallerRunsPolicy:当前拒绝策略会在线程池无法处理任务时,将任务交给调用者处理(这个是在下常用的)。
  • DiscardPolicy:当前拒绝策略会在线程池无法处理任务时,直接将任务丢弃掉。
  • DiscardOldestPolicy:当前拒绝策略会在线程池无法处理任务时,将队列中最早的任务丢弃掉,将当前任务再次尝试交给线程池处理。
  • 自定义Policy:根据自己的业务,可以将任务扔到数据库或者缓存,也可以做其他操作。

线程池的执行流程

面试官:你了解过线程池的工作流程是怎样的吗?

一个新的任务提交到线程池时,线程池的处理流程如下:

线程池判断核心线程池里的线程是否都在执行任务。如果不是,创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则进入下个流程。

线程池判断阻塞队列是否已满。如果阻塞队列没有满,则将新提交的任务存储在阻塞队列中。如果阻塞队列已满,则进入下个流程。

线程池判断线程池里的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已满,则交给饱和策略来处理这个任务。 image.png

使用线程池要注意些什么

面试官:看来你对线程池理解的挺不错,能说说使用线程池有什么要注意的点吗?

Executors 提供的很多方法默认使用的都是无界的 LinkedBlockingQueue,高负载情境下,无界队列很容易导致 OOM,而 OOM 会导致所有请求都无法处理,这是致命问题。所以强烈建议使用有界队列

使用有界队列,当任务过多时,线程池会触发执行拒绝策略,线程池默认的拒绝策略会 throw RejectedExecutionException 这是个运行时异常,对于运行时异常编译器并不强制 catch 它,所以开发人员很容易忽略。因此默认拒绝策略要慎重使用。如果线程池处理的任务非常重要,建议自定义自己的拒绝策略;并且在实际工作中,自定义的拒绝策略往往和降级策略配合使用。

使用线程池,还要注意异常处理的问题,例如通过 ThreadPoolExecutor 对象的 execute() 方法提交任务时,如果任务在执行的过程中出现运行时异常,会导致执行任务的线程终止;不过,最致命的是任务虽然异常了,但是你却获取不到任何通知,这会让你误以为任务都执行得很正常。虽然线程池提供了很多用于异常处理的方法,但是最稳妥和简单的方案还是捕获所有异常并按需处理。

面试官:嗯,小伙子,看来你对线程池的基础知识掌握的还不错,以后CRUD的活可以放心地交给你了。