Java线程池面试要点

简介: Java线程池在面试的时候问的挺多的,曾经我就在面试过程中两次被问到,面试官通过面试者对线程池的理解回答也能大致了解到面试者的实际开发经验如何,以及对多线程的理解运用有没有深入到位。

Java线程池在面试的时候问的挺多的,曾经我就在面试过程中两次被问到,面试官通过面试者对线程池的理解回答也能大致了解到面试者的实际开发经验如何,以及对多线程的理解运用有没有深入到位。



同时,面试官在切入多线程问题的时候通常也不会太过生硬,而是一步一步通过线程创建方式、线程状态切换、线程协同引导过来,整体谈下来其实也挺花时间的,会触及到多线程的方方面面,但对开发者素质确实也是一番不小的考验,今天我们也不完全铺开去描述,就仅仅针对线程池这一点来聊聊面试的时候会碰到的一些问题。


ThreadPoolExecutor参数含义


ThreadPoolExecutor 构造函数参数定义我们可以直接在 concurrent 包当中找到。


public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    }

这几个核心参数的含义分别是:

  1. corePoolSize:线程池核心线程数量,核心线程不会被回收,即使没有任务执行,也会保持空闲状态,设置 allowCoreThreadTimeOut 参数为 true 才会进行回收。如果线程池中的线程少于此数目,则在执行任务时创建。
  2. maximumPoolSize:线程池最大线程数,表示在线程池中最多能创建多少个线程。当线程数量达到corePoolSize,且workQueue队列塞满任务了之后,继续创建线程,当线程池中的线程数量到达这个数字时,新来的任务会执行拒绝策略。
  3. keepAliveTime:表示线程没有任务执行时最多能保持多少时间会被回收,注意,这个参数控制的是超过corePoolSize之后的“临时线程”的存活时间。
  4. unit:参数 keepAliveTime 的时间单位。
  5. workQueue:工作队列,存放提交的等待任务,其中有队列大小的限制。
  6. threadFactory:创建线程的工厂类,通常我们会自定义一个threadFactory设置线程的名称,这样我们就可以知道线程是由哪个工厂类创建的,可以快速定位排查问题。
  7. handler:如果线程池已满,新的任务进来时的拒绝策略。


ThreadPoolExecutor 参数含义是最常见的一个问题,如果面试者对这些参数比较了解,至少说明面试者在多线程运用层面不会存在太大的问题,反之,如果面试官提示某个参数后面试者还是一脸懵的话,那么基础印象分就会大打折扣。


线程池线程创建的流程是怎样的


线程池线程创建的时机可以用下面这张图简单表示。


image.png


线程创建流程是这样的:


  1. 如果当前运行的线程少于corePoolSize(核心线程数),则创建新线程来执行任务(执行这一步骤需要获取全局锁)。
  2. 如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue(阻塞队列/任务队列)。
  3. 如果无法将任务加入BlockingQueue(队列已满),则在非corePool中创建新的线程来处理任务(执行这一步骤也需要获取全局锁)。
  4. 如果创建新线程将使得当前运行的线程超出maximumPoolSize限制,任务将被拒绝,并执行线程饱和策略,如:RejectedExecutionHandler.rejectedExecution()方法。


注意:初始化线程池时,线程数为0。


工作列队有哪几种实现

存放任务的工作队列有6种主要的实现,分别是 ArrayBlockingQueue、LinkedBlockingQueue、LinkedBlockingDeque、PriorityBlockingQueue、DelayQueue、SynchronousQueue。它们的区别如下:

  1. ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列(数组结构可配合指针实现一个环形队列)。
  2. LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列,在未指明容量时,容量默认为 Integer.MAX_VALUE
  3. LinkedBlockingDeque:使用双向队列实现的双端阻塞队列,双端意味着可以像普通队列一样 FIFO(先进先出),可以以像栈一样 FILO(先进后出)
  4. PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列,对元素没有要求,可以实现 Comparable 接口也可以提供 Comparator 来对队列中的元素进行比较,跟时间没有任何关系,仅仅是按照优先级取任务。
  5. DelayQueue:同 PriorityBlockingQueue,也是二叉堆实现的优先级阻塞队列。要求元素都实现 Delayed 接口,通过执行时延从队列中提取任务,时间没到任务取不出来。
  6. SynchronousQueue:一个不存储元素的阻塞队列,消费者线程调用 take() 方法的时候就会发生阻塞,直到有一个生产者线程生产了一个元素,消费者线程就可以拿到这个元素并返回;生产者线程调用put()方法的时候就会发生阻塞,直到有一个消费者线程消费了一个元素,生产者才会返回。


拒绝策略有哪几种


线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满了,再也塞不下新任务了。这时候我们就需要拒绝策略机制合理地处理新进来的任务。JDK 内置的四种拒绝策略如下:


  1. AbortPolicy(默认):丢弃任务并抛出 RejectedExecutionException 异常。
  2. CallerRunsPolicy:由调用线程处理该任务。(例如io操作,线程消费速度没有NIO快,可能导致阻塞队列一直增加,此时可以使用这个模式)。
  3. DiscardPolicy:丢弃任务,但是不抛出异常。(可以配合这种模式进行自定义的处理方式)。
  4. DiscardOldestPolicy:丢弃队列最早的未处理任务,然后重新尝试执行任务。


线程池的分类


Java 里面线程池的顶级接口是 Executor,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具,真正的线程池接口是  ExecutorService。Java中 Executors 工厂类可以为我们自动创建不同策略配置的线程池,供我们直接使用。


 newCachedThreadPool

coreSize 线程数0,最大线程数无限制,线程的允许空闲时间是60s,阻塞队列是 SynchronousQueue。适用于“短任务”情况。由于采用SynchronousQueue,每当提交一个任务,都会超过阻塞队列的长度,导致创建新线程处理,所以说:每当提交一个任务,都会创建一个线程,可能造成OOM。此外,线程空闲1分钟就会销毁,所以该线程池可能会频繁地创建和销毁线程。
 newFixedThreadPool


coreSize 和最大线程数都是用户输入的,阻塞队列用的 LinkedBlockingQueue,线程的允许空闲时间是0s。其核心特性就是线程数不会增加,不会减少,线程池也不会自己销毁。由于阻塞队列是无限大的,不会执行拒绝策略。所以可能会堆积无限的请求,导致OOM。


 newSingleThreadExecutor


相当于线程数为1的 newFixedThreadPool,缺点和 newFixedThreadPool 一样。有的小伙伴可能会问,那它和单个线程有什么区别?


newSingleThreadExecutor Thread

任务执行完成后,不会自动销毁,可以复用

任务执行完成后,会自动销毁

可以将任务存储在阻塞队列中,逐个执行

无法存储任务,只能执行一个任务


 newScheduledThreadPool


支持定时及周期性任务执行,需要注意的是,如果任务执行过程中抛出了异常就会停止执行任务,而且也不会再周期地执行该任务了。所以如果想保持任务周期执行,需要 catch 一切可能的异常。


 newWorkStealingPool


采用的 ForkJoin 框架,可以将任务进行分割,同时线程之间会互相帮助。另外,阻塞队列采用的 LinkedBlockingDeque,可以进行任务窃取。由于实际使用不多,这里只作了解。


实际使用时并不推荐这样去直接创建使用,阿里Java开发规约里面也有相应约束:


【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。说明:Executors 返回的线程池对象的弊端如下: 1)FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。 2)CachedThreadPool 和 ScheduledThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。


如何关闭线程池


 shutdown(高安全低响应)


本质上执行的是 interrupt 方法,阻止新来的任务提交,会将线程池的状态改成 SHUTDOWN,当再执行 execute 提交任务时,如果测试到状态不为 RUNNING,则执行拒绝策略,从而达到阻止新任务提交的目的。对于已经提交的任务不会产生任何影响,当已经提交的任务执行完以后,它会将那些闲置的线程进行中断,这个过程是异步的,也就是说只会打断空闲线程,如果当前还有任务队列还有任务未执行,线程将继续把任务执行完。


 shutdownNow(低安全高响应)


将阻止新来的任务提交,同时将线程池的状态改成 STOP,当再执行 execute 提交任务时,如果测试到状态不为 RUNNING,则抛出 rejectedExecution,从而达到阻止新任务提交的目的。该方法会中断空闲进程,同时也会中断当前正在运行的线程,即 workers 中的线程。如果遇到已经激活的任务,并且处于阻塞状态时,shutdownNow() 会执行1次中断阻塞的操作,此时对应的线程报 InterruptedException,如果后续还要等待某个资源,则按正常逻辑等待某个资源的到达。例如,一个线程正在 sleep 状态中,此时执行 shutdownNow(),它向该线程发起 interrupt() 请求,而 sleep() 方法遇到有 interrupt() 请求时,会抛出 InterruptedException(),并继续往下执行。在这里要提醒注意的是,在激活的任务中,如果有多个 sleep(),该方法只会中断第一个sleep(),而后面的仍然按照正常的执行逻辑进行。


两张关闭线程池的方式的主要区别用一句话概括就是:高安全低响应体现在 shutdown 等待任务执行完成再关闭,可以保证任务一定被执行,但是关闭线程池需要等待较长的时间;低安全高响应体现在 shutdownNow 会关闭正在执行任务的线程,任务可能并没有执行完毕,也不会回退到任务队列中,将会消失,但是关闭线程池不需要等待较长的时间。


线程池核心线程数经验配置

CPU密集型任务:尽量压榨CPU,参考值设置为CPU的个数+1。IO密集型任务:参考值可以设置为CPU的个数 ✖️ 2。

以上只是经验配置参考,具体的使用配置如果在条件允许的情况下最好使用公司的压测工具或环境压测一下。


使用线程池有什么好处

  1. 线程重用:线程的创建和销毁开销是巨大的,而通过线程池的重用大大减少了这些不必要的开销,当然既然少了这么多开销,其线程执行速度也是突飞猛进的提升。
  2. 控制线程池的并发数:线程不是并发的越多,性能越高,反而在线程并发太多时,线程的切换会消耗系统大量的资源,可以通过设置线程池最大并发线程数目,维持系统高性能。
  3. 线程池可以对线程进行管理:虽然线程提供了线程组操控线程,但是线程池拥有更多管理线程的API。
  4. 可以储存需要执行的任务:当任务提交过多时,可以将任务储存起来,等待线程处理。


最后

以上就是关于线程池的一些核心要点了,单从使用的角度来说,有些细节不用了解的太深入,看完也就忘了。但从面试的角度来说还是需要尽量了解全面一些,至少得对得起简历上那句“对技术有追求”不是?关于线程池今天就介绍这么多,有其他遗漏需要补充的欢迎留言讨论。

团队介绍

我们是阿里巴巴淘系技术部的新品平台技术团队, 依托于淘系大数据正在建立一套完整的涵盖消费者洞察、宏观及细分市场分析、竞争分析、市场策略研究、产品创新机制等的新品研发和创新孵化平台, 为品牌、商家及行业提供规模化的新品孵化和运营能力, 沉淀新品孵化机制和运营策略, 最终建立起一套基于大数据驱动的从市场研究、新品研发到新品投放营销的全链路新品运营平台。发送邮件到tianhang.th#alibaba-inc.com(发送邮件时,请把#替换成@)

相关文章
|
3月前
|
算法 Java
50道java集合面试题
50道 java 集合面试题
|
6月前
|
缓存 Java 关系型数据库
2025 年最新华为 Java 面试题及答案,全方位打造面试宝典
Java面试高频考点与实践指南(150字摘要) 本文系统梳理了Java面试核心考点,包括Java基础(数据类型、面向对象特性、常用类使用)、并发编程(线程机制、锁原理、并发容器)、JVM(内存模型、GC算法、类加载机制)、Spring框架(IoC/AOP、Bean生命周期、事务管理)、数据库(MySQL引擎、事务隔离、索引优化)及分布式(CAP理论、ID生成、Redis缓存)。同时提供华为级实战代码,涵盖Spring Cloud Alibaba微服务、Sentinel限流、Seata分布式事务,以及完整的D
377 1
|
5月前
|
缓存 Java API
Java 面试实操指南与最新技术结合的实战攻略
本指南涵盖Java 17+新特性、Spring Boot 3微服务、响应式编程、容器化部署与数据缓存实操,结合代码案例解析高频面试技术点,助你掌握最新Java技术栈,提升实战能力,轻松应对Java中高级岗位面试。
494 0
|
5月前
|
Java 数据库连接 数据库
Java 相关知识点总结含基础语法进阶技巧及面试重点知识
本文全面总结了Java核心知识点,涵盖基础语法、面向对象、集合框架、并发编程、网络编程及主流框架如Spring生态、MyBatis等,结合JVM原理与性能优化技巧,并通过一个学生信息管理系统的实战案例,帮助你快速掌握Java开发技能,适合Java学习与面试准备。
264 2
Java 相关知识点总结含基础语法进阶技巧及面试重点知识
|
3月前
|
算法 Java
50道java基础面试题
50道java基础面试题
|
6月前
|
算法 架构师 Java
Java 开发岗及 java 架构师百度校招历年经典面试题汇总
以下是百度校招Java岗位面试题精选摘要(150字): Java开发岗重点关注集合类、并发和系统设计。HashMap线程安全可通过Collections.synchronizedMap()或ConcurrentHashMap实现,后者采用分段锁提升并发性能。负载均衡算法包括轮询、加权轮询和最少连接数,一致性哈希可均匀分布请求。Redis持久化有RDB(快照恢复快)和AOF(日志更安全)两种方式。架构师岗涉及JMM内存模型、happens-before原则和无锁数据结构(基于CAS)。
195 5
|
6月前
|
安全 Java API
2025 年 Java 校招面试常见问题及详细答案汇总
本资料涵盖Java校招常见面试题,包括Java基础、并发编程、JVM、Spring框架、分布式与微服务等核心知识点,并提供详细解析与实操代码,助力2025校招备战。
329 1
|
5月前
|
缓存 Java 关系型数据库
Java 面试经验总结与最新 BAT 面试资料整理含核心考点的 Java 面试经验及最新 BAT 面试资料
本文汇总了Java面试经验与BAT等大厂常见面试考点,涵盖心态准备、简历优化、面试技巧及Java基础、多线程、JVM、数据库、框架等核心技术点,并附实际代码示例,助力高效备战Java面试。
204 0
|
5月前
|
缓存 Cloud Native Java
Java 面试微服务架构与云原生技术实操内容及核心考点梳理 Java 面试
本内容涵盖Java面试核心技术实操,包括微服务架构(Spring Cloud Alibaba)、响应式编程(WebFlux)、容器化(Docker+K8s)、函数式编程、多级缓存、分库分表、链路追踪(Skywalking)等大厂高频考点,助你系统提升面试能力。
288 0