大家好,这篇文章主要跟大家聊下Java线程池面试中可能会问到的一些问题。 全程干货,耐心看完,相信你能轻松应对各种线程池面试问题,同时也能让你对线程池有更深一步的了解。 相信各位Javaer在面试中或多或少肯定被问到过线程池相关问题吧,线程池是一个相对比较复杂的体系,基于此可以问出各种各样、五花八门的问题。 若你很熟悉线程池,如果可以,完全可以滔滔不绝跟面试官扯一个小时线程池,一般面试也就一个小时左右,那么这样留给面试官问其他问题的时间就很少了,或者其他问题可能问的也就不深入了,那你通过面试的几率是不就更大点了呢。 下面我们开始列下线程池面试可能会被问到的问题以及该怎么回答,以下只是参考答案,你可以加入自己的理解。1。面试官:日常工作中有用到线程池吗?什么是线程池?为什么要使用线程池? 一般面试官考察你线程池相关知识前,大概率会先问这个问题,如果你说没用过,不了解,ok,那就没以下问题啥事了,估计你的面试结果肯定也凶多吉少了。 作为JUC包下的门面担当,线程池是名副其实的JUC一哥,不了解线程池,那说明你对JUC包其他工具也了解的不咋样吧,对JUC没深入研究过,那就是没掌握到Java的精髓,给面试官这样一个印象,那结果可想而知了。 所以说,这一分一定要吃下,那我们应该怎么回答好这问题呢? 可以这样说: 计算机发展到现在,摩尔定律在现有工艺水平下已经遇到难易突破的物理瓶颈,通过多核CPU并行计算来提升服务器的性能已经成为主流,随之出现了多线程技术。 线程作为操作系统宝贵的资源,对它的使用需要进行控制管理,线程池就是采用池化思想(类似连接池、常量池、对象池等)管理线程的工具。 JUC给我们提供了ThreadPoolExecutor体系类来帮助我们更方便的管理线程、并行执行任务。 下图是Java线程池继承体系: 顶级接口Executor提供了一种方式,解耦任务的提交和执行,只定义了一个execute(Runnablecommand)方法用来提交任务,至于具体任务怎么执行则交给他的实现者去自定义实现。 ExecutorService接口继承Executor,且扩展了生命周期管理的方法、返回Futrue的方法、批量提交任务的方法。 AbstractExecutorService抽象类继承ExecutorService接口,对ExecutorService相关方法提供了默认实现,用RunnableFuture的实现类FutureTask包装Runnable任务,交给execute()方法执行,然后可以从该FutureTask阻塞获取执行结果,并且对批量任务的提交做了编排。 ThreadPoolExecutor继承AbstractExecutorService,采用池化思想管理一定数量的线程来调度执行提交的任务,且定义了一套线程池的生命周期状态,用一个ctl变量来同时保存当前池状态(高3位)和当前池线程数(低29位)。看过源码的小伙伴会发现,ThreadPoolExecutor类里的方法大量有同时需要获取或更新池状态和池当前线程数的场景,放一个原子变量里,可以很好的保证数据的一致性以及代码的简洁性,说到ctl了,可以顺便讲下几个状态之间的流转过程。用此变量保存当前池状态(高3位)和当前线程数(低29位)privatefinalAtomicIntegerctlnewAtomicInteger(ctlOf(RUNNING,0));privatestaticfinalintCOUNTBITSInteger。SIZE3;privatestaticfinalintCAPACITY(1COUNTBITS)1;runStateisstoredinthehighorderbits可以接受新任务提交,也会处理任务队列中的任务结果:111跟29个0:11100000000000000000000000000000privatestaticfinalintRUNNING1COUNTBITS;不接受新任务提交,但会处理任务队列中的任务结果:00000000000000000000000000000000privatestaticfinalintSHUTDOWN0COUNTBITS;不接受新任务,不执行队列中的任务,且会中断正在执行的任务结果:00100000000000000000000000000000privatestaticfinalintSTOP1COUNTBITS;任务队列为空,workerCount0,线程池的状态在转换为TIDYING状态时,会执行钩子方法terminated()结果:01000000000000000000000000000000privatestaticfinalintTIDYING2COUNTBITS;调用terminated()钩子方法后进入TERMINATED状态结果:01000000000000000000000000000000privatestaticfinalintTERMINATED3COUNTBITS;Packingandunpackingctl低29位变为0,得到了线程池的状态privatestaticintrunStateOf(intc){returncCAPACITY;}高3位变为为0,得到了线程池中的线程数privatestaticintworkerCountOf(intc){returncCAPACITY;}privatestaticintctlOf(intrs,intwc){} 使用线程池可以带来以下好处:降低资源消耗。降低频繁创建、销毁线程带来的额外开销,复用已创建线程 降低使用复杂度。将任务的提交和执行进行解耦,我们只需要创建一个线程池,然后往里面提交任务就行,具体执行流程由线程池自己管理,降低使用复杂度 提高线程可管理性。能安全有效的管理线程资源,避免不加限制无限申请造成资源耗尽风险 提高响应速度。任务到达后,直接复用已创建好的线程执行 线程池的使用场景简单来说可以有:快速响应用户请求,响应速度优先。比如一个用户请求,需要通过RPC调用好几个服务去获取数据然后聚合返回,此场景就可以用线程池并行调用,响应时间取决于响应最慢的那个RPC接口的耗时;又或者一个注册请求,注册完之后要发送短信、邮件通知,为了快速返回给用户,可以将该通知操作丢到线程池里异步去执行,然后直接返回客户端成功,提高用户体验。 单位时间处理更多请求,吞吐量优先。比如接受MQ消息,然后去调用第三方接口查询数据,此场景并不追求快速响应,主要利用有限的资源在单位时间内尽可能多的处理任务,可以利用队列进行任务的缓冲。 基于以上使用场景,可以套到自己项目中,说下为了提升系统性能,自己对负责的系统模块使用线程池做了哪些优化,优化前后对比Qps提升多少、Rt降低多少、服务器数量减少多少等等。2。面试官:ThreadPoolExecutor都有哪些核心参数? 其实一般面试官问你这个问题并不是简单听你说那几个参数,更多的是想听你描述下线程池执行流程。 青铜回答: 包含核心线程数(corePoolSize)、最大线程数(maximumPoolSize),空闲线程超时时间(keepAliveTime)、时间单位(unit)、阻塞队列(workQueue)、拒绝策略(handler)、线程工厂(ThreadFactory)这7个参数。 这个回答基本上也没毛病,但只能60分飘过。 钻石回答: 回答完包含这几个参数之后,会再主动描述下线程池的执行流程,也就是execute()方法执行流程。 execute()方法执行逻辑如下:publicvoidexecute(Runnablecommand){if(commandnull)thrownewNullPointerException();intcctl。get();if(workerCountOf(c)corePoolSize){if(addWorker(command,true))cctl。get();}if(isRunning(c)workQueue。offer(command)){intrecheckctl。get();if(!isRunning(recheck)remove(command))reject(command);elseif(workerCountOf(recheck)0)addWorker(null,false);}elseif(!addWorker(command,false))reject(command);} 可以总结出如下主要执行流程,当然看上述代码会有一些异常分支判断,可以自己梳理加到下述执行主流程里判断线程池的状态,如果不是RUNNING状态,直接执行拒绝策略 如果当前线程数核心线程池,则新建一个线程来处理提交的任务 如果当前线程数核心线程数且任务队列没满,则将任务放入阻塞队列等待执行 如果核心线程池当前线程池数最大线程数,且任务队列已满,则创建新的线程执行提交的任务 如果当前线程数最大线程数,且队列已满,则执行拒绝策略拒绝该任务 这个回答就比较能体现出你的悟性,能主动描述线程池执行流程,说明你对线程池还是比较了解的,在面试官心里就会留下还行的印象,这也是你要面高级Java必须要达到的最低要求,这个回答拿个75分应该问题不大。 王者回答: 在回答完包含哪些参数及execute方法的执行流程后。然后可以说下这个执行流程是JUC标准线程池提供的执行流程,主要用在CPU密集型场景下。 像Tomcat、Dubbo这类框架,他们内部的线程池主要用来处理网络IO任务的,所以他们都对JUC线程池的执行流程进行了调整来支持IO密集型场景使用。 他们提供了阻塞队列TaskQueue,该队列继承LinkedBlockingQueue,重写了offer()方法来实现执行流程的调整。Overridepublicbooleanoffer(Runnableo){wecantdoanychecksif(parentnull)returnsuper。offer(o);wearemaxedoutonthreads,simplyqueuetheobjectif(parent。getPoolSize()parent。getMaximumPoolSize())returnsuper。offer(o);wehaveidlethreads,justaddittothequeueif(parent。getSubmittedCount()(parent。getPoolSize()))returnsuper。offer(o);ifwehavelessthreadsthanmaximumforcecreationofanewthreadif(parent。getPoolSize()parent。getMaximumPoolSize())ifwereachedhere,weneedtoaddittothequeuereturnsuper。offer(o);} 可以看到他在入队之前做了几个判断,这里的parent就是所属的线程池对象1。如果parent为null,直接调用父类offer方法入队 2。如果当前线程数等于最大线程数,则直接调用父类offer()方法入队 3。如果当前未执行的任务数量小于等于当前线程数,仔细思考下,是不是说明有空闲的线程呢,那么直接调用父类offer()入队后就马上有线程去执行它 4。如果当前线程数小于最大线程数量,则直接返回false,然后回到JUC线程池的执行流程回想下,是不是就去添加新线程去执行任务了呢 5。其他情况都直接入队 具体可以看之前写过的这篇文章 动态线程池(DynamicTp)之动态调整Tomcat、Jetty、Undertow线程池参数篇 可以看出当当前线程数大于核心线程数时,JUC原生线程池首先是把任务放到队列里等待执行,而不是先创建线程执行。 如果Tomcat接收的请求数量大于核心线程数,请求就会被放到队列中,等待核心线程处理,如果并发量很大,就会在队列里堆积大量任务,这样会降低请求的总体响应速度。 所以Tomcat并没有使用JUC原生线程池,利用TaskQueue的offer()方法巧妙的修改了JUC线程池的执行流程,改写后Tomcat线程池执行流程如下:判断如果当前线程数小于核心线程池,则新建一个线程来处理提交的任务 如果当前当前线程池数大于核心线程池,小于最大线程数,则创建新的线程执行提交的任务 如果当前线程数等于最大线程数,则将任务放入任务队列等待执行 如果队列已满,则执行拒绝策略 而且Tomcat会做核心线程预热,在创建好线程池后接着会去创建核心线程并启动,服务启动后就可以直接接受客户端请求进行处理了,避免了冷启动问题。 然后再说下线程池的Worker线程模型,继承AQS实现了锁机制。线程启动后执行runWorker()方法,runWorker()方法中调用getTask()方法从阻塞队列中获取任务,获取到任务后先执行beforeExecute()钩子函数,再执行任务,然后再执行afterExecute()钩子函数。若超时获取不到任务会调用processWorkerExit()方法执行Worker线程的清理工作。 相信这一通回答后,拿个90分应该问题不大了。 runworker()、getTask()、addWorker()等源码解读可以看之前写的文章: 线程池源码解析3。面试官:你刚也说到了Worker继承AQS实现了锁机制,那ThreadPoolExecutor都用到了哪些锁?为什么要用锁? 这个问题比较刁钻,一般准备过程中可能不太会注意,那下面我们来一起看下用到了那些锁。 1)mainLock锁 ThreadPoolExecutor内部维护了ReentrantLock类型锁mainLock,在访问workers成员变量以及进行相关数据统计记账(比如访问largestPoolSize、completedTaskCount)时需要获取该重入锁。 面试官:为什么要有mainLock?privatefinalReentrantLockmainLocknewReentrantLock();Setcontainingallworkerthreadsinpool。AccessedonlywhenholdingmainLock。privatefinalHashSetWorkerworkersnewHashSetWorker();Trackslargestattainedpoolsize。AccessedonlyundermainLock。privateintlargestPoolSCounterforcompletedtasks。Updatedonlyonterminationofworkerthreads。AccessedonlyundermainLock。privatelongcompletedTaskC 可以看到workers变量用的HashSet是线程不安全的,是不能用于多线程环境的。largestPoolSize、completedTaskCount也是没用volatile修饰,所以需要在锁的保护下进行访问。 面试官:为什么不直接用个线程安全容器呢? 其实Doug老爷子在mainLock变量的注释上解释了,意思就是说事实证明,相比于线程安全容器,此处更适合用lock,主要原因之一就是串行化interruptIdleWorkers()方法,避免了不必要的中断风暴。 面试官:怎么理解这个中断风暴呢? 其实简单理解就是如果不加锁,interruptIdleWorkers()方法在多线程访问下就会发生这种情况。一个线程调用interruptIdleWorkers()方法对Worker进行中断,此时该Worker出于中断中状态,此时又来一个线程去中断正在中断中的Worker线程,这就是所谓的中断风暴。 面试官:那largestPoolSize、completedTaskCount变量加个volatile关键字修饰是不是就可以不用mainLock了? 这个其实Doug老爷子也考虑到了,其他一些内部变量能用volatile的都加了volatile修饰了,这两个没加主要就是为了保证这两个参数的准确性,在获取这两个值时,能保证获取到的一定是修改方法执行完成后的值。如果不加锁,可能在修改方法还没执行完成时,此时来获取该值,获取到的就是修改前的值,然后修改方法一提交,就会造成获取到的数据不准确了。 2)Worker线程锁 刚也说了Worker线程继承AQS,实现了Runnable接口,内部持有一个Thread变量,一个firstTask,及completedTasks三个成员变量。 基于AQS的acquire()、tryAcquire()实现了lock()、tryLock()方法,类上也有注释,该锁主要是用来维护运行中线程的中断状态。在runWorker()方法中以及刚说的interruptIdleWorkers()方法中用到了。 面试官:这个维护运行中线程的中断状态怎么理解呢?protectedbooleantryAcquire(intunused){if(compareAndSetState(0,1)){setExclusiveOwnerThread(Thread。currentThread());}}publicvoidlock(){acquire(1);}publicbooleantryLock(){returntryAcquire(1);} 在runWorker()方法中获取到任务开始执行前,需要先调用w。lock()方法,lock()方法会调用tryAcquire()方法,tryAcquire()实现了一把非重入锁,通过CAS实现加锁。protectedbooleantryAcquire(intunused){if(compareAndSetState(0,1)){setExclusiveOwnerThread(Thread。currentThread());}} interruptIdleWorkers()方法会中断那些等待获取任务的线程,会调用w。tryLock()方法来加锁,如果一个线程已经在执行任务中,那么tryLock()就获取锁失败,就保证了不能中断运行中的线程了。 重点:所以Worker继承AQS主要就是为了实现了一把非重入锁,维护线程的中断状态,保证不能中断运行中的线程。4。面试官:你在项目中是怎样使用线程池的?Executors了解吗? 这里面试官主要想知道你日常工作中使用线程池的姿势,现在大多数公司都在遵循阿里巴巴Java开发规范,该规范里明确说明不允许使用Executors创建线程池,而是通过ThreadPoolExecutor显示指定参数去创建。 你可以这样说,知道Executors工具类,很久之前有用过,也踩过坑,Executors创建的线程池有发生OOM的风险。 Executors。newFixedThreadPool和Executors。SingleThreadPool创建的线程池内部使用的是无界(Integer。MAXVALUE)的LinkedBlockingQueue队列,可能会堆积大量请求,导致OOM。 Executors。newCachedThreadPool和Executors。scheduledThreadPool创建的线程池最大线程数是用的Integer。MAXVALUE,可能会创建大量线程,导致OOM。 自己在日常工作中也有封装类似的工具类,但是都是内存安全的,参数需要自己指定适当的值,也有基于LinkedBlockingQueue实现了内存安全阻塞队列MemorySafeLinkedBlockingQueue,当系统内存达到设置的最大剩余阈值时,就不在往队列里添加任务了,避免发生OOM。publicstaticThreadPoolExecutornewFixedThreadPool(StringthreadPrefix,intpoolSize,intqueueCapacity){returnThreadPoolBuilder。newBuilder()。corePoolSize(poolSize)。maximumPoolSize(poolSize)。workQueue(QueueTypeEnum。MEMORYSAFELINKEDBLOCKINGQUEUE。getName(),queueCapacity,null)。threadFactory(threadPrefix)。buildDynamic();}publicstaticExecutorServicenewCachedThreadPool(StringthreadPrefix,intmaximumPoolSize){returnThreadPoolBuilder。newBuilder()。corePoolSize(0)。maximumPoolSize(maximumPoolSize)。workQueue(QueueTypeEnum。SYNCHRONOUSQUEUE。getName(),null,null)。threadFactory(threadPrefix)。buildDynamic();}publicstaticThreadPoolExecutornewThreadPool(StringthreadPrefix,intcorePoolSize,intmaximumPoolSize,intqueueCapacity){returnThreadPoolBuilder。newBuilder()。corePoolSize(corePoolSize)。maximumPoolSize(maximumPoolSize)。workQueue(QueueTypeEnum。MEMORYSAFELINKEDBLOCKINGQUEUE。getName(),queueCapacity,null)。threadFactory(threadPrefix)。buildDynamic();} 我们一般都是在Spring环境中使用线程池的,直接使用JUC原生ThreadPoolExecutor有个问题,Spring容器关闭的时候可能任务队列里的任务还没处理完,有丢失任务的风险。 我们知道Spring中的Bean是有生命周期的,如果Bean实现了Spring相应的生命周期接口(InitializingBean、DisposableBean接口),在Bean初始化、容器关闭的时候会调用相应的方法来做相应处理。 所以最好不要直接使用ThreadPoolExecutor在Spring环境中,可以使用Spring提供的ThreadPoolTaskExecutor,或者DynamicTp框架提供的DtpExecutor线程池实现。 也会按业务类型进行线程池隔离,各任务执行互不影响,避免共享一个线程池,任务执行参差不齐,相互影响,高耗时任务会占满线程池资源,导致低耗时任务没机会执行;同时如果任务之间存在父子关系,可能会导致死锁的发生,进而引发OOM。 使用线程池的常规操作是通过Bean定义多个业务隔离的线程池实例。我们是参考美团线程池实践那篇文章做了一个动态可监控线程池的轮子,而且利用了Spring的一些特性,将线程池实例都配置在配置中心里,服务启动的时候会从配置中心拉取配置然后生成BeanDefination注册到Spring容器中,在Spring容器刷新时会生成线程池实例注册到Spring容器中。这样我们业务代码就不用显式用Bean声明线程池了,可以直接通过依赖注入的方式使用线程池,而且也可以动态调整线程池的参数了。 更多使用姿势参考之前发的文章: 线程池,我是谁?我在哪儿?5。面试官:刚你说到了通过ThreadPoolExecutor来创建线程池,那核心参数设置多少合适呢? 这个问题该怎么回答呢? 可能很多人都看到过《Java并发编程实践》这本书里介绍的一个线程数计算公式: NcpuCPU核数 Ucpu目标CPU利用率,0Ucpu1 WC等待时间计算时间 要程序跑到CPU的目标利用率,需要的线程数为: NthreadsNcpuUcpu(1WC) 这公式太偏理论化了,很难实际落地下来,首先很难获取准确的等待时间和计算时间。再着一个服务中会运行着很多线程,比如Tomcat有自己的线程池、Dubbo有自己的线程池、GC也有自己的后台线程,我们引入的各种框架、中间件都有可能有自己的工作线程,这些线程都会占用CPU资源,所以通过此公式计算出来的误差一定很大。 所以说怎么确定线程池大小呢? 其实没有固定答案,需要通过压测不断的动态调整线程池参数,观察CPU利用率、系统负载、GC、内存、RT、吞吐量等各种综合指标数据,来找到一个相对比较合理的值。 所以不要再问设置多少线程合适了,这个问题没有标准答案,需要结合业务场景,设置一系列数据指标,排除可能的干扰因素,注意链路依赖(比如连接池限制、三方接口限流),然后通过不断动态调整线程数,测试找到一个相对合适的值。6。面试官:你们线程池是咋监控的? 因为线程池的运行相对而言是个黑盒,它的运行我们感知不到,该问题主要考察怎么感知线程池的运行情况。 可以这样回答: 我们自己对线程池ThreadPoolExecutor做了一些增强,做了一个线程池管理框架。主要功能有监控告警、动态调参。主要利用了ThreadPoolExecutor类提供的一些set、get方法以及一些钩子函数。 动态调参是基于配置中心实现的,核心参数配置在配置中心,可以随时调整、实时生效,利用了线程池提供的set方法。 监控,主要就是利用线程池提供的一些get方法来获取一些指标数据,然后采集数据上报到监控系统进行大盘展示。也提供了Endpoint实时查看线程池指标数据。 同时定义了5中告警规则。线程池活跃度告警。活跃度activeCountmaximumPoolSize,当活跃度达到配置的阈值时,会进行事前告警。 队列容量告警。容量使用率queueSizequeueCapacity,当队列容量达到配置的阈值时,会进行事前告警。 拒绝策略告警。当触发拒绝策略时,会进行告警。 任务执行超时告警。重写ThreadPoolExecutor的afterExecute()和beforeExecute(),根据当前时间和开始时间的差值算出任务执行时长,超过配置的阈值会触发告警。 任务排队超时告警。重写ThreadPoolExecutor的beforeExecute(),记录提交任务时时间,根据当前时间和提交时间的差值算出任务排队时长,超过配置的阈值会触发告警 通过监控告警可以让我们及时感知到我们业务线程池的执行负载情况,第一时间做出调整,防止事故的发生。7。面试官:execute()提交任务和submit()提交任务有啥不同? 看到这个问题,是不是大多数人都觉得这个我行。execute()无返回值,submit()有返回值,会返回一个FutureTask,然后可以调用get()方法阻塞获取返回值。 这样回答只能算及格,其实面试官问你这个问题主要想听你讲下FutureTask的实现原理,FutureTask继承体系如下: 我们调用submit()方法提交的任务(RunnableorCallable)会被包装成FutureTask()对象。FutureTask类提供了7种任务状态和五个成员变量。Possiblestatetransitions:NEWCOMPLETINGNORMALNEWCOMPLETINGEXCEPTIONALNEWCANCELLEDNEWINTERRUPTINGINTERRUPTED构造函数中state置为NEW,初始态privatestaticfinalintNEW0;瞬时态,表示完成中privatestaticfinalintCOMPLETING1;正常执行结束后的状态privatestaticfinalintNORMAL2;异常执行结束后的状态privatestaticfinalintEXCEPTIONAL3;调用cancel方法成功执行后的状态privatestaticfinalintCANCELLED4;瞬时态,中断中privatestaticfinalintINTERRUPTING5;正常执行中断后的状态privatestaticfinalintINTERRUPTED6;任务状态,以上7种通过submit()提交的任务,执行完后置为nullprivateCallableV任务执行结果或者调用get()要抛出的异常privateOnonvolatile,protectedbystatereadswrites执行任务的线程,会在run()方法中通过cas赋值privatevolatileT调用get()后由等待线程组成的无锁并发栈,通过cas实现无锁privatevolatileWaitN 创建FutureTask对象时state置为NEW,callable赋值为我们传入的任务。 run()方法中会去执行callable任务。执行之前先判断任务处于NEW状态并且通过cas设置runner为当前线程成功。然后去调用call()执行任务,执行成功后会调用set()方法将结果赋值给outcome,任务执行抛出异常后会将异常信息调用setException()赋值给outcome。至于为什么要先将状态变为COMPLETING,再变为NORMAL,主要是为了保证在NORMAL态时已经完成了outcome赋值。finishCompletion()会去唤醒(通过LockSupport。unpark())那些因调用get()而阻塞的线程(waiters)。protectedvoidset(Vv){if(UNSAFE。compareAndSwapInt(this,stateOffset,NEW,COMPLETING)){UNSAFE。putOrderedInt(this,stateOffset,NORMAL);finalstatefinishCompletion();}} 调用get()方法会阻塞获取结果(或异常),如果stateCOMPLETING,说明任务已经执行完成(NORMAL、EXCEPTIONAL、CANCELLED、INTERRUPTED),则直接通过report()方法返回结果或抛出异常。如果stateCOMPLETING,说明任务还在执行中或还没开始执行,则调用awaitDone()方法进行阻塞等待。publicVget(longtimeout,TimeUnitunit)throwsInterruptedException,ExecutionException,TimeoutException{if(unitnull)thrownewNullPointerException();if(sCOMPLETING(sawaitDone(true,unit。toNanos(timeout)))COMPLETING)thrownewTimeoutException();returnreport(s);} awaitDone()方法则通过state状态判断来决定直接返回还是将当前线程添加到waiters里,然后调用LockSupport。park()方法挂起当前线程。 还有个重要的cancel()方法,因为FutureTask源码类注释的第一句就说了FutureTask是一个可取消的异步计算。代码也非常简单,如果state不是NEW或者通过CAS赋值为INTERRUPTINGCANCELLED失败则直接返回。反之如果mayInterruptIfRunningture,表示可能中断在运行中线程,则中断线程,state变为INTERRUPTED,最后去唤醒等待的线程。publicbooleancancel(booleanmayInterruptIfRunning){if(!(stateNEWUNSAFE。compareAndSwapInt(this,stateOffset,NEW,mayInterruptIfRunning?INTERRUPTING:CANCELLED)))try{incasecalltointerruptthrowsexceptionif(mayInterruptIfRunning){try{Tif(t!null)t。interrupt();}finally{finalstateUNSAFE。putOrderedInt(this,stateOffset,INTERRUPTED);}}}finally{finishCompletion();}} 以上简单介绍了下FutureTask的执行流程,篇幅有限,源码解读的不是很仔细,后面可以考虑单独出一篇文章好好分析下FutureTask的源码。8。面试官:什么是阻塞队列?阻塞队列有哪些? 阻塞队列BlockingQueue继承Queue,是我们熟悉的基本数据结构队列的一种特殊类型。 当从阻塞队列中获取数据时,如果队列为空,则等待直到队列有元素存入。当向阻塞队列中存入元素时,如果队列已满,则等待直到队列中有元素被移除。提供offer()、put()、take()、poll()等常用方法。 JDK提供的阻塞队列的实现有以下前7种: 1)ArrayBlockingQueue:由数组实现的有界阻塞队列,该队列按照FIFO对元素进行排序。维护两个整形变量,标识队列头尾在数组中的位置,在生产者放入和消费者获取数据共用一个锁对象,意味着两者无法真正的并行运行,性能较低。 2)LinkedBlockingQueue:由链表组成的有界阻塞队列,如果不指定大小,默认使用Integer。MAXVALUE作为队列大小,该队列按照FIFO对元素进行排序,对生产者和消费者分别维护了独立的锁来控制数据同步,意味着该队列有着更高的并发性能。 3)SynchronousQueue:不存储元素的阻塞队列,无容量,可以设置公平或非公平模式,插入操作必须等待获取操作移除元素,反之亦然。 4)PriorityBlockingQueue:支持优先级排序的无界阻塞队列,默认情况下根据自然序排序,也可以指定Comparator。 5)DelayQueue:支持延时获取元素的无界阻塞队列,创建元素时可以指定多久之后才能从队列中获取元素,常用于缓存系统或定时任务调度系统。 6)LinkedTransferQueue:一个由链表结构组成的无界阻塞队列,与LinkedBlockingQueue相比多了transfer和tryTranfer方法,该方法在有消费者等待接收元素时会立即将元素传递给消费者。 7)LinkedBlockingDeque:一个由链表结构组成的双端阻塞队列,可以从队列的两端插入和删除元素。 8)VariableLinkedBlockingQueue:说完以上JDK提供这几个阻塞队列后,还可以说下LinkedBlockingQueue是我们使用最广泛的阻塞队列,但是LinkedBlockingQueue一旦定义好后是不能修改容量capacity的。自己在使用线程池的过程中有动态去调整容量的需求,所以参考RabbitMq里的VariableLinkedBlockingQueue,实现了一个可以调整容量的增强版LinkedBlockingQueue,实现容量的动态调整。 9)MemorySafeLinkedBlockingQueue:而且LinkedBlockingQueue默认是使用Integer。MAXVALUE作为容量的,也就是个无界队列,可能会有发生OOM的风险,所以自己实现了一个内存安全的MemorySafeLinkedBlockingQueue,可以配置最大剩余内存,当内存达到该值的时候,再往队列放任务就会失败,很好的保证了不会发生令人头疼的OOM问题。 10)TaskQueue:上面讲Tomcat线程池时说过该阻塞队列,作为LinkedBlockingQueue的子类,覆写了offer()、poll()、take()等方法来调整线程池的执行流程。 重点说下8、9这两个自定义阻塞队列,来突出你对阻塞队列丰富的使用经验,这两队列源码可以看以下地址: MemorySafeLinkedBlockingQueueVariableLinkedBlockingQueue队列实现 9。面试官:线程池拒绝策略有哪些?适用场景是怎么样的? 当阻塞队列已满并且达到最大线程数时,再提交任务会走拒绝策略流程,JDK提供了拒绝策略顶层接口RejectedExecutionHandler,所有拒绝策略都需要继承该接口,JDK内置了四种拒绝策略。 1)AbortPolicy:线程池默认的拒绝策略,触发时会抛出RejectedExecutionException异常。如果是一些比较重要的业务,可以使用该拒绝策略,在系统不能进一步支持更大并发量的情况下通过抛出异常及时发现问题并进行处理。 2)CallerRunsPolicy:在线程池没关闭的情况下,由调用者线程去处理任务,反之直接丢弃。此拒绝策略追求任务都能被执行,不丢失,比较适合并发量不大并且不允许丢失任务的场景场景,性能较低。 3)DiscardPolicy:丢弃任务,不抛出异常,一般无感知。建议一些无关紧要的任务可以使用此策略。 4)DiscardOldestPolicy:丢弃队列中最老的任务,然后重新提交被拒绝的任务。需要根据业务场景进行选择是否要用。 3、4这两种拒绝策略都在会在无感知的情况下丢弃任务,需要根据业务场景决定是否要使用。 也可以根据自己需要自定义拒绝策略,比如Dubbo定义了拒绝策略AbortPolicyWithReport,在抛出异常前会先进行线程堆栈信息的打印。10。面试官:你在使用线程池的过程中遇到过哪些坑或者需要注意的地方? 这个问题其实也是在考察你对一些细节的掌握程度,就全甩锅给年轻刚毕业没经验的自己就行。可以适当多说些,也证明自己对线程池有着丰富的使用经验。 1)OOM问题。刚开始使用线程都是通过Executors创建的,前面说了,这种方式创建的线程池会有发生OOM的风险,可以举例说明。 2)任务执行异常丢失问题。可以通过下述4种方式解决在任务代码中增加try、catch异常处理 如果使用的Future方式,则可通过Future对象的get方法接收抛出的异常 为工作线程设置setUncaughtExceptionHandler,在uncaughtException方法中处理异常 可以重写afterExecute(Runnabler,Throwablet)方法,拿到异常t 3)共享线程池问题。整个服务共享一个全局线程池,导致任务相互影响,耗时长的任务占满资源,短耗时任务得不到执行。同时父子线程间会导致死锁的发生,进而导致OOM。 4)跟ThreadLocal配合使用,导致脏数据问题。我们知道Tomcat利用线程池来处理收到的请求,会复用线程,如果我们代码中用到了ThreadLocal,在请求处理完后没有去remove,那每个请求就有可能获取到之前请求遗留的脏值。 5)ThreadLocal在线程池场景下会失效,可以考虑用阿里开源的Ttl来解决。 6)需要自定义线程工厂指定线程名称,不然发生问题都不知道咋定位。 以上提到的线程池动态调参、通知告警在开源动态线程池项目DynamicTp中已经实现了,可以直接引入到自己项目中使用。关于DynamicTp DynamicTp是一个基于配置中心实现的轻量级动态线程池管理工具,主要功能可以总结为动态调参、通知报警、运行监控、三方包线程池管理等几大类。 经过多个版本迭代,目前最新版本v1。0。8具有以下特性 特性代码零侵入:所有配置都放在配置中心,对业务代码零侵入轻量简单:基于springboot实现,引入starter,接入只需简单4步就可完成,顺利3分钟搞定高可扩展:框架核心功能都提供SPI接口供用户自定义个性化实现(配置中心、配置文件解析、通知告警、监控数据采集、任务包装等等)线上大规模应用:参考美团线程池实践,美团内部已经有该理论成熟的应用经验多平台通知报警:提供多种报警维度(配置变更通知、活性报警、容量阈值报警、拒绝触发报警、任务执行或等待超时报警),已支持企业微信、钉钉、飞书报警,同时提供SPI接口可自定义扩展实现监控:定时采集线程池指标数据,支持通过MicroMeter、JsonLog日志输出、Endpoint三种方式,可通过SPI接口自定义扩展实现任务增强:提供任务包装功能,实现TaskWrapper接口即可,如MdcTaskWrapper、TtlTaskWrapper、SwTraceTaskWrapper,可以支持线程池上下文信息传递兼容性:JUC普通线程池和Spring中的ThreadPoolTaskExecutor也可以被框架监控,Bean定义时加DynamicTp注解即可可靠性:框架提供的线程池实现Spring生命周期方法,可以在Spring容器关闭前尽可能多的处理队列中的任务多模式:参考Tomcat线程池提供了IO密集型场景使用的EagerDtpExecutor线程池支持多配置中心:基于主流配置中心实现线程池参数动态调整,实时生效,已支持Nacos、Apollo、Zookeeper、Consul、Etcd,同时也提供SPI接口可自定义扩展实现中间件线程池管理:集成管理常用第三方组件的线程池,已集成Tomcat、Jetty、Undertow、Dubbo、RocketMq、Hystrix等组件的线程池管理(调参、监控报警) 原文链接:https:mp。weixin。qq。comsfz5di9zAjVzqI0NcKLc7g