当前位置: 首页 > 后端技术 > Java

Java线程池面试要点

时间:2023-04-01 14:07:55 Java

作者:AugustRush来源:淘系科技Java线程池在面试的时候被问的比较多。面试的时候被问了两次。面试官通过面试官对线程池的理解回答也能大致了解面试官的实际开发经验,对多线程的理解和应用是否到位。同时,面试官在切入多线程问题时通常不会太生硬,而是通过线程创建方法、线程状态切换、线程协调等方式一步步引导。整体的讨论其实是需要时间的,会触及多线程的方方面面,但是对于开发者的素质确实是一个很大的考验。今天我们就不全面描述了,只是说说基于线程池在面试中会遇到的一些问题。ThreadPoolExecutor参数含义ThreadPoolExecutor的构造函数参数定义可以直接在并发包中找到。publicThreadPoolExecutor(intcorePoolSize,intmaximumPoolSize,longkeepAliveTime,TimeUnitunit,BlockingQueueworkQueue,ThreadFactorythreadFactory,RejectedExecutionHandlerhandler){}这些核心参数的含义是:corePoolSize:线程池的核心线程数,核心线程数会被回收,即使没有任务执行,也会一直闲置。将allowCoreThreadTimeOut参数设置为true以进行回收。如果线程池中的线程数少于此数量,则在执行任务时创建。maximumPoolSize:线程池最大线程数,表示线程池中最多可以创建的线程数。当线程数达到corePoolSize且workQueue队列已满任务时,继续创建线程。当线程池中的线程数量达到这个数量时,新任务将执行拒绝策略。keepAliveTime:表示线程在没有任务执行时最多能保持多久,将被回收。注意这个参数控制的是超过corePoolSize后“临时线程”的存活时间。unit:参数keepAliveTime的时间单位。workQueue:工作队列,存放提交的等待任务,队列有大小限制。threadFactory:创建线程的工厂类。通常我们会自定义一个threadFactory来设置线程的名称,这样我们就可以知道线程是由哪个工厂类创建的,可以快速定位和排查问题。handler:如果线程池满了,新任务进来时的拒绝策略。ThreadPoolExecutor参数的含义是最常见的问题。如果面试官对这些参数比较了解,至少说明面试官在多线程的应用上不会有太多的问题。如果你傻眼了,你的基本印象分会大大降低。线程池线程创建的过程是怎样的?线程池线程创建的时机可以简单的用下图表示。线程创建过程如下:如果当前运行的线程小于corePoolSize(核心线程数),则创建一个新的线程来执行任务(执行这一步需要全局锁)。如果正在运行的线程等于或大于corePoolSize,则将任务加入BlockingQueue(阻塞队列/任务队列)。如果任务无法加入BlockingQueue(队列已满),则在非corePool中新建线程处理任务(执行这一步还需要获取全局锁)。如果创建新的线程会导致当前运行的线程超过maximumPoolSize的限制,则拒绝任务,执行线程饱和策略,如:RejectedExecutionHandler.rejectedExecution()方法。注意:初始化线程池时,线程数为0。工作队列有哪几种?存储任务的工作队列主要有六种实现,分别是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工厂类可以自动创建不同策略配置的线程池供我们直接使用。?newCachedThreadPoolcoreSize线程数为0,最大线程数无限制,线程允许空闲时间为60s,阻塞队列为SynchronousQueue。适用于“短任务”的情况。由于使用了SynchronousQueue,每次提交任务都会超过阻塞队列的长度,导致创建新的线程进行处理。所以说每次提交任务都会创建一个线程,可能会导致OOM。另外,线程空闲1分钟后就会销毁,所以线程池可能会频繁创建和销毁线程。?newFixedThreadPoolcoreSize和最大线程数由用户输入,用于阻塞队列的LinkedBlockingQueue,线程允许空闲时间为0s。它的核心特点是线程数不会增减,线程池不会自行销毁。由于阻塞队列是无限的,因此不会强制执行拒绝策略。所以它可能会堆积无限的请求,导致OOM。?newSingleThreadExecutor相当于1个线程的newFixedThreadPool,缺点和newFixedThreadPool一样。可能有朋友会问,它和单线程有什么区别?newSingleThreadExecutorThread任务执行后,不会自动销毁。它可以重复使用。任务执行后,会自动销毁。任务可以存放在阻塞队列中,不能一个一个地存放任务。只能执行一个任务?newScheduledThreadPool支持定时任务和周期任务执行,需要注意的是,如果在任务执行过程中抛出异常,就会停止任务的执行,不会周期性地执行任务。所以如果要保持任务循环执行,就需要捕获所有可能的异常。?newWorkStealingPool采用的ForkJoin框架,可以划分任务,线程之间会互相帮助。另外,阻塞队列使用的LinkedBlockingDeque可以进行任务窃取。由于实际使用不多,这里仅作了解。在实际使用中,不建议直接通过这种方式创建和使用。阿里Java开发规范中有相应的约束:【强制】不允许使用Executors创建线程池,必须通过ThreadPoolExecutor创建。这种处理方式让写的同学更加清楚线程池的运行规律,避免了资源耗尽的风险。注意:Executors返回的线程池对象的缺点如下:1)FixedThreadPool和SingleThreadPool:允许的请求队列长度为Integer.MAX_VALUE,可能累积大量请求,导致OOM。2)CachedThreadPool和ScheduledThreadPool:允许创建的线程数为Integer.MAX_VALUE,可能会创建大量线程,导致OOM。线程池如何关闭?shutdown(高安全低响应)本质上是执行了中断方法,阻止了新任务的提交,将线程池的状态变为SHUTDOWN。如果是RUNNING,则执行拒绝策略,阻止新任务的提交。它不会对提交的任务产生任何影响。当提交的任务被执行时,它会中断那些空闲的线程。这个过程是异步的,这意味着只有空闲线程才会被中断。如果当前任务队列中还有任务没有执行完,线程会继续执行任务。?shutdownNow(低安全高响应)会阻止新任务的提交,同时改变线程池的状态为STOP。执行execute提交任务时,如果测试状态不是RUNNING,会抛出rejectedExecution,达到阻塞新任务提交的目的。该方法会中断空闲进程,也会中断当前正在运行的线程,即worker中的线程。如果遇到激活的任务,并且处于阻塞状态,shutdownNow()会执行一次中断阻塞操作。这时候对应的线程就会报InterruptedException。如果它还在等待某个资源,它就会按照正常的逻辑等待某个资源。资源到货。比如一个线程处于休眠状态,此时执行shutdownNow(),它向线程发起interrupt()请求,当sleep()方法遇到interrupt()请求时,会抛出InterruptedException(),继续下一步执行。这里需要提醒的是,如果active任务中有多个sleep(),该方法只会打断第一个sleep(),后面的仍然按照正常的执行逻辑。两种关闭线程池的方式的主要区别用一句话概括:安全性高,响应性低,体现在shutdown是等待任务执行完再关闭,可以保证任务一定会执行,但是关闭线程池需要很长时间;lowsecurity高响应体现在shutdownNow会关闭正在执行任务的线程。任务可能没有完成,也不会返回到任务队列,会消失,但是关闭线程池不需要等待很长时间。线程池核心线程数经验配置,CPU密集型任务:尽量压榨CPU,参考值设置为CPU数+1。IO密集型任务:参考值可以设置为CPU个数??2.以上只是经验配置的参考。如果条件允许,最好使用公司的压力测试工具或针对特定使用配置的环境压力测试。使用线程池有什么好处?线程复用:线程创建和销毁的成本是巨大的,线程池的复用大大减少了这些不必要的成本。当然,由于损失的成本少了很多,线程的执行速度也得到了突飞猛进的提升。.控制线程池并发线程数:线程并发越多,性能越高。相反,当并发线程过多时,线程的切换会消耗大量的系统资源。可以通过设置线程池的最大并发线程数来保持系统的高性能。线程池可以管理线程:虽然线程提供了线程组来控制线程,但是线程池有更多的管理线程的API。可以存储需要执行的任务:当提交的任务过多时,可以将任务存储起来,等待线程处理。最后,以上是关于线程池的一些核心点。单从使用的角度来说,有些细节不需要理解太深,看完就忘得一干二净。但是从面试的角度来说,还是要尽量了解的全面,至少要对得起简历上的那句“追求技术”吧?今天关于线程池的介绍就这么多了。如有其他遗漏需要补充,欢迎留言讨论。整理了一些学习资料,包括Java基础、Android进阶、架构设计、NDK、音视频开发、跨平台、底层源码等技术,还有一线大厂最新面试题合集2022年,我会和你分享。帮助大家扫清学习道路上的障碍~你的能力会得到提升,思维也会得到开阔~如果你需要,可以点击下方链接免费获取。链接:https://shimo.im/docs/R13j85m...