持续创作,加速成长!这是我参与「掘金日新计划 · 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:根据自己的业务,可以将任务扔到数据库或者缓存,也可以做其他操作。
线程池的执行流程
面试官:你了解过线程池的工作流程是怎样的吗?
一个新的任务提交到线程池时,线程池的处理流程如下:
线程池判断核心线程池里的线程是否都在执行任务。如果不是,创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则进入下个流程。
线程池判断阻塞队列是否已满。如果阻塞队列没有满,则将新提交的任务存储在阻塞队列中。如果阻塞队列已满,则进入下个流程。
线程池判断线程池里的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已满,则交给饱和策略来处理这个任务。
使用线程池要注意些什么
面试官:看来你对线程池理解的挺不错,能说说使用线程池有什么要注意的点吗?
Executors 提供的很多方法默认使用的都是无界的 LinkedBlockingQueue,高负载情境下,无界队列很容易导致 OOM,而 OOM 会导致所有请求都无法处理,这是致命问题。所以强烈建议使用有界队列。
使用有界队列,当任务过多时,线程池会触发执行拒绝策略,线程池默认的拒绝策略会 throw RejectedExecutionException 这是个运行时异常,对于运行时异常编译器并不强制 catch 它,所以开发人员很容易忽略。因此默认拒绝策略要慎重使用。如果线程池处理的任务非常重要,建议自定义自己的拒绝策略;并且在实际工作中,自定义的拒绝策略往往和降级策略配合使用。
使用线程池,还要注意异常处理的问题,例如通过 ThreadPoolExecutor 对象的 execute() 方法提交任务时,如果任务在执行的过程中出现运行时异常,会导致执行任务的线程终止;不过,最致命的是任务虽然异常了,但是你却获取不到任何通知,这会让你误以为任务都执行得很正常。虽然线程池提供了很多用于异常处理的方法,但是最稳妥和简单的方案还是捕获所有异常并按需处理。
面试官:嗯,小伙子,看来你对线程池的基础知识掌握的还不错,以后CRUD的活可以放心地交给你了。