JAVA线程池连环问
来源:JAVA线程池连环问
Java 线程池连环问口播面试文案
面试场景
面试官(以下简称 “面”):同学你好!今天咱们就围绕 Java 线程池这块知识聊一聊,主要看你对这部分的掌握程度,不用紧张,想到什么就说什么就行。
求职者(以下简称 “求”):好的面试官,我会尽力回答。
面试问题与互动
面:那咱们先从基础开始,你能简单说说 “Java 线程池” 到底是什么吗?它主要能解决咱们开发中的什么问题呀?
求:我理解的话,Java 线程池就是提前创建好一批线程放在 “池子” 里,等有任务要执行的时候,就从池子里拿个线程去跑任务;任务跑完了,这个线程也不直接销毁,而是放回池子里,等着下一个任务来用。它主要解决的是 “频繁创建 / 销毁线程” 的问题 —— 因为创建和销毁线程特别耗 CPU 和内存,要是每次有任务都新创一个线程,频繁操作下来系统性能会变低;而且线程池还能控制同时运行的线程数量,避免线程太多把系统资源占满,另外也方便咱们统一管理和监控这些线程。
面:解释得很清楚!那你知道,创建线程池的时候,有哪些核心参数是必须要考虑的吗?每个参数大概是干嘛用的,能说说不?
求:主要有 7 个核心参数吧,我一个个说:第一个是 “核心线程数(corePoolSize)”,就是池子里始终要保留的线程数量,哪怕这些线程闲着没事干也不删;第二个是 “最大线程数(maximumPoolSize)”,就是池子最多能创建多少个线程;第三个是 “工作队列(workQueue)”,要是核心线程都在忙,新任务就先放这个队列里等着;第四个是 “线程存活时间(keepAliveTime)”,指的是非核心线程闲着的时候能留多久,超过这个时间就会被销毁;第五个是 “时间单位(unit)”,就是存活时间的单位,比如秒、毫秒这些;第六个是 “线程工厂(threadFactory)”,用来创建新线程的,咱们还能通过它自定义线程的名字、优先级;最后一个是 “拒绝策略(handler)”,要是线程池和工作队列都满了,新任务接不过来,就靠这个策略处理。
面:对这些参数的理解很到位!那结合这些参数,你能跟我说说,一个新任务提交到线程池后,整个处理流程是咋样的不?比如从任务交进去,到线程执行,中间是怎么一步步走的?
求:流程大概是这样:新任务过来后,先看池子里的线程数够不够核心线程数 —— 要是不够,就直接创个核心线程来跑这个任务;要是核心线程都满了,就把任务丢到工作队列里等着;要是队列也满了,再看当前线程数够不够最大线程数,不够的话就创个非核心线程来执行任务;要是最大线程数也满了,那就只能执行拒绝策略了。等任务跑完,核心线程会留在池子里等新任务,非核心线程要是过了存活时间还没新任务,就会被销毁,直到池子里的线程数回到核心线程数。
面:说得很有条理!不过这里我多问一句 —— 你刚才说 “任务跑完线程放回池子里等新任务”,那线程池到底是怎么做到 “线程复用” 的呀?比如线程为啥能反复执行不同任务,而不是跑一个任务就没了?
求:这个我知道,关键是线程池里的线程会 “循环拿任务”。线程池里的每个线程其实都是一个 “Worker” 对象,Worker 里会装一个实际的线程;这个线程启动后,会进入一个死循环 —— 不停从工作队列里拿任务(用 take () 方法,要是队列里没任务,线程就会阻塞等着,不浪费资源);拿到任务后,就执行任务的 run () 方法;等任务执行完,又回到循环里,接着拿新任务。直到线程池要关闭,或者这个线程被中断了,这个循环才会结束,线程才会真正销毁。简单说就是:线程不 “干完一个就走”,而是 “蹲在池子里反复接活干”,这样就实现复用了。
面:原来是这样!解释得很通俗。那咱们再接着聊,你知道线程池常见的 “拒绝策略” 有哪些不?每种策略大概适合啥场景用呀?
求:常见的有 4 种,都是 ThreadPoolExecutor 里的内部类。第一种是 AbortPolicy,这是默认的 —— 要是接不了新任务,直接抛个 RejectedExecutionException 异常,适合那种 “任务不能丢,必须严格处理” 的场景,比如支付相关的任务;第二种是 CallerRunsPolicy,让提交任务的那个线程自己去执行这个任务,比如主线程提交任务,就主线程自己跑,这样能放慢提交任务的速度,适合并发量不算大,又不想丢任务的场景;第三种是 DiscardPolicy,直接把接不了的新任务扔了,还不抛异常,适合那种 “任务丢了也没事” 的场景,比如日志收集这类非核心任务;第四种是 DiscardOldestPolicy,把队列里等最久的任务扔了,再把新任务放进去,适合任务有时效性的场景,比如实时数据统计,旧数据不如新数据有用的情况。
面:那在实际项目里,你是怎么配置线程池参数的呀?比如核心线程数和最大线程数,你会根据啥来定?
求:主要看任务类型。要是 “CPU 密集型任务”,比如复杂的计算、数据处理这类 —— 这类任务主要耗 CPU,线程多了反而会频繁切换上下文,拖慢速度,所以核心线程数和最大线程数一般设成 “CPU 核心数 + 1”,或者就等于 CPU 核心数;要是 “IO 密集型任务”,比如查数据库、发网络请求这类 —— 任务执行时会等 IO(比如等数据库返回结果),这时候线程可以去干别的,所以线程数能设多些,一般是 CPU 核心数的 2 到 4 倍。另外还得看内存,要是内存紧张,也不能设太多线程,不然容易内存溢出。工作队列也一样,任务多、执行快就用有界队列,避免堆太多任务;任务少、执行慢就用无界队列,让任务慢慢等。
面:那 Java 里常见的线程池实现有哪些呀?比如 Executors 类里提供的那些,它们各有啥特点,适合啥场景用?
求:Executors 里主要有 4 种。第一种是 FixedThreadPool,固定线程数的 —— 核心线程数和最大线程数一样,用的是无界的 LinkedBlockingQueue,适合任务数量固定、需要长期跑的场景,比如后台定时处理数据,但要注意:要是任务执行慢,队列会堆很多任务,容易内存溢出;第二种是 CachedThreadPool,可缓存的 —— 核心线程数是 0,最大线程数是 Integer.MAX_VALUE,用的是同步队列,线程闲 60 秒就销毁,适合任务多但执行快的场景,比如处理短期请求,但要是任务执行慢,会创一大堆线程,把资源占满;第三种是 SingleThreadExecutor,单线程的 —— 核心和最大线程数都是 1,也是无界队列,适合要 “任务按顺序执行” 的场景,比如日志写入文件,不能并发写乱了;第四种是 ScheduledThreadPool,定时任务用的 —— 核心线程数固定,最大是 Integer.MAX_VALUE,用的是 DelayedWorkQueue,适合定时或周期性任务,比如每天凌晨备份数据、每隔 5 分钟发一次消息。
面:那你用这些线程池的时候,有没有遇到过问题?或者说,你觉得这些线程池潜在的风险有哪些?
求:风险还挺明显的。比如 FixedThreadPool 和 SingleThreadExecutor,用的是无界队列,要是任务提交得比执行得快,队列会一直堆任务,最后内存溢出;CachedThreadPool 和 ScheduledThreadPool,最大线程数是 Integer.MAX_VALUE,要是任务执行慢,会创成千上万的线程,CPU 和内存直接就扛不住了。所以实际项目里,一般不建议直接用 Executors 创建线程池,而是自己用 ThreadPoolExecutor 手动创建 —— 这样能自己设有界队列、合理的最大线程数,避免这些风险。
面:那要是线程池里的线程跑任务时抛了异常,会有啥影响呀?线程池会怎么处理这个线程?
求:要是抛的是 “没捕获的异常”,这个线程会被销毁,然后线程池会新创一个线程补上来 —— 比如核心线程抛了未捕获异常,就新创个核心线程维持数量;非核心线程抛了,就直接销毁,等有新任务需要时再新创。但要是咱们在任务里把异常捕获处理了,那线程跑完任务后会正常放回池子里,不会被销毁。所以用线程池的时候,一定要记得在任务里捕获异常,不然线程频繁销毁再创建,会影响性能。
面:最后一个问题哈,你知道怎么监控线程池的运行状态不?比如想知道现在有多少线程在忙、多少任务在等,能通过啥方式获取这些信息?
求:主要有两种方式。第一种是用 ThreadPoolExecutor 自带的方法,比如 getCorePoolSize () 能看核心线程数,getPoolSize () 看当前总线程数,getActiveCount () 看正在忙的线程数,getQueue () 能拿到工作队列,再用 size () 看队列里待执行的任务数,getCompletedTaskCount () 看已经跑完的任务数;第二种是自定义监控,比如在任务执行前、执行后记录日志,或者用第三方工具,像 Spring Boot Admin、Prometheus 这些,能实时看线程池状态,比如任务堆多了、线程数异常了,能及时发现,好调整参数。
面:好的,今天关于线程池的问题就问到这,你的回答很全面,逻辑也很清晰,后续有结果会通知你。
求:谢谢面试官!