线程池高频面试题(整理版)

一、基础核心类与工作原理

1. 线程池的核心工作原理是什么?(高频必问)

核心是「复用线程、控制并发量」,避免频繁创建/销毁线程的性能开销,执行流程分4步:

  1. 提交任务后,先判断核心线程数(corePoolSize)是否已满:未满则创建核心线程执行任务;已满则将任务加入阻塞队列。

  2. 若阻塞队列也已满,判断最大线程数(maximumPoolSize)是否已满:未满则创建非核心线程执行任务;已满则执行拒绝策略。

  3. 非核心线程在空闲超过保活时间(keepAliveTime)后会被回收;核心线程默认永久存活(可通过配置允许超时回收)。

2. ThreadPoolExecutor 的核心构造参数有哪些?每个参数的作用是什么?

ThreadPoolExecutor 是线程池的核心实现类,7个核心构造参数缺一不可,具体含义如下:

参数名称 具体含义
corePoolSize 核心线程数,线程池长期保有的最小活跃线程数,默认空闲时不回收(可通过 allowCoreThreadTimeOut 开启超时回收)。
maximumPoolSize 线程池允许的最大线程数,包含核心线程和非核心线程,是线程数的上限。
keepAliveTime 非核心线程空闲时的存活时间,超过该时间则被回收;若开启核心线程超时,也适用于核心线程。
unit keepAliveTime 的时间单位,常用 TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)。
workQueue 阻塞队列,用于存储核心线程已满时等待执行的任务,常用有 ArrayBlockingQueue(有界)、LinkedBlockingQueue(可无界)。
threadFactory 线程工厂,用于统一创建线程,可自定义线程名、优先级、守护线程状态,便于排查线程相关问题。
handler 拒绝策略,当线程池(最大线程数已满)和阻塞队列都满时,处理新提交任务的策略(如丢弃、抛出异常)。

二、实战配置与最佳实践

3. 如何合理配置线程池的线程数量?(生产实战高频)

核心取决于任务类型,结合 CPU 核心数计算,避免线程过多导致上下文切换,或过少导致资源浪费:

  • CPU 密集型任务(如计算、排序):线程数 = CPU 核心数 + 1。+1 是为了利用 CPU 空闲间隙,减少线程等待,提升利用率。

  • IO 密集型任务(如文件读写、网络请求、数据库操作):线程数 = 2 * CPU 核心数。因任务大部分时间在等待 IO,多线程可提高并发效率;更精准公式:线程数 = CPU 核心数 / (1 - 阻塞系数)(阻塞系数通常为 0.8~0.9)。

  • 混合任务:将任务拆分为 CPU 密集型和 IO 密集型,分别配置独立线程池;或通过压测调整线程数,找到最优值。

4. 为什么不建议用 Executors 创建线程池,推荐用 ThreadPoolExecutor?

Executors 提供的快捷方法(如 newFixedThreadPool、newCachedThreadPool)存在资源耗尽风险,不符合生产环境要求;ThreadPoolExecutor 可显式控制所有参数,更安全可控:

  • newFixedThreadPool:使用无界队列(LinkedBlockingQueue),任务过多时会不断堆积,导致内存溢出(OOM)。

  • newCachedThreadPool:最大线程数为 Integer.MAX_VALUE,任务激增时会创建大量线程,导致 CPU 占用过高或 OOM。

  • ThreadPoolExecutor:可手动设置队列容量、最大线程数、拒绝策略,能根据业务场景灵活配置,避免上述风险。

5. 核心线程数(corePoolSize)会动态变化吗?

核心线程数(corePoolSize)本身是固定配置,不会自动变化;但实际存活的核心线程数量会动态调整:

  • 参数层面:corePoolSize 是初始化时设定的阈值,运行期间需手动调用 setCorePoolSize() 方法才能修改。

  • 运行层面:默认情况下,核心线程创建后会永久存活(空闲时阻塞等待任务),实际存活数稳定在 corePoolSize;若调用 allowCoreThreadTimeOut(true),核心线程空闲超过 keepAliveTime 也会被回收,此时实际存活数可在 0~corePoolSize 之间波动。

  • 注意:线程本身没有"核心/非核心"的标记,只是数量 ≤ corePoolSize 时创建的线程视为核心线程,超过则为非核心线程,非核心线程不会升级为核心线程。

三、常用方法与区别

6. submit() 和 execute() 方法的区别?(高频)

两者均用于提交任务,核心区别体现在返回值、异常处理和任务类型上:

  • 返回值:execute() 无返回值;submit() 返回 Future 对象,可通过 get() 方法获取任务执行结果或异常。

  • 异常处理:execute() 中任务抛出的未捕获异常会直接打印到控制台;submit() 会捕获异常,需通过 Future.get() 才能获取异常(封装在 ExecutionException 中)。

  • 任务类型:execute() 仅能接收 Runnable 类型任务;submit() 可接收 Runnable 或 Callable 类型任务(Callable 可返回结果)。

7. shutdown() 和 shutdownNow() 的区别?

两者均用于关闭线程池,核心区别在于"关闭方式"和"对任务的处理":

  • shutdown()(优雅关闭):① 不再接受新任务;② 会执行完阻塞队列中已有的任务;③ 正在执行的任务不会被中断,线程执行完任务后逐步回收。

  • shutdownNow()(强制关闭):① 不再接受新任务;② 尝试中断正在执行的任务(调用线程的 interrupt() 方法);③ 清空阻塞队列,返回未执行的任务列表;④ 若线程未响应中断(如未处理中断标志位),可能无法立即停止。

8. 调用 shutdown() 或 shutdownNow() 后,线程一定会退出吗?

不一定,取决于线程的执行状态和中断响应逻辑:

  • shutdown():正在执行的任务会继续执行至完成,线程才会退出;队列中的任务执行完毕后,所有线程逐步回收。

  • shutdownNow():仅触发线程的 interrupt() 方法,若线程中没有响应中断的逻辑(如未使用 sleep()、wait(),且未检查中断标志位),线程会继续执行任务,不会立即退出。

四、底层细节与问题处理

9. 线程池为什么要使用阻塞队列?

阻塞队列是线程池实现"线程复用"和"任务缓冲"的核心,主要作用有3点:

  • 任务缓冲:当核心线程已满时,暂存任务,避免直接丢弃,提升任务执行率。

  • 线程复用:线程执行完任务后,会阻塞在队列的 take() 方法上,等待新任务,避免线程空转消耗 CPU。

  • 解耦:将任务提交(生产者)和任务执行(消费者)分离,简化线程池的设计和维护。

10. 线程池的线程是如何实现复用的?

核心是"线程循环获取任务",避免执行完单个任务后销毁,具体逻辑:

  • 核心线程:执行完任务后,会永久阻塞在 workQueue.take() 方法上,等待新任务到来,一直循环执行"获取任务→执行任务"。

  • 非核心线程:执行完任务后,会阻塞在 workQueue.poll(keepAliveTime, unit) 方法上,若超时未获取到新任务,则退出循环,线程被回收。

11. 线程池中的线程如何被回收?

线程回收分两种场景,核心取决于线程类型和配置:

  • 非核心线程:空闲时间超过 keepAliveTime,从队列获取任务超时,退出循环,线程终止并被回收。

  • 核心线程:默认不回收;若调用 allowCoreThreadTimeOut(true),则核心线程空闲超过 keepAliveTime 后,也会被回收。

  • 特殊情况:线程池调用 shutdown() 或 shutdownNow() 后,所有线程会在执行完任务(或被中断)后逐步回收。

12. 如何处理线程池中的任务异常?

分两种提交方式,对应不同的异常处理方案,覆盖生产中常见场景:

  • execute() 提交:① 在任务内部用 try-catch 捕获异常,自行处理(如日志记录);② 自定义 ThreadFactory,给线程设置 UncaughtExceptionHandler,统一处理未捕获异常。

  • submit() 提交:① 通过 Future.get() 捕获 ExecutionException,获取任务抛出的异常;② 结合 try-catch 处理异常,避免异常被隐藏。

  • 全局处理:实现 RejectedExecutionHandler 处理拒绝策略相关的异常;或通过 Thread.UncaughtExceptionHandler 统一处理线程池所有未捕获异常。

13. 如何确保线程池中的任务按特定顺序执行?

4种常用方案,根据业务场景选择:

  • 单线程池:使用 Executors.newSingleThreadExecutor(),任务按提交顺序依次执行,无并发冲突。

  • 有序队列:使用 PriorityBlockingQueue 作为阻塞队列,给任务设置优先级,线程池按优先级执行任务。

  • Future 依赖:提交任务后,通过 Future.get() 等待前一个任务执行完成,再提交下一个任务,强制顺序执行。

  • CompletableFuture 链式编排:使用 CompletableFuture 的 thenRun()、thenAccept() 等方法,实现任务的顺序执行、依赖执行。

14. 线程工厂在线程池中的作用是什么?

核心是"统一管理线程创建",主要作用有3点:

  • 封装创建逻辑:统一创建线程,避免重复代码,降低维护成本。

  • 自定义线程属性:可设置线程名(如"thread-pool-order-1")、优先级、守护线程状态、异常处理器,便于排查线程问题(如日志定位)。

  • 业务隔离:不同业务模块使用不同的线程工厂,创建不同标识的线程,便于监控和管理(如区分订单、支付相关线程)。

15. 非核心线程能成为核心线程吗?

不能。线程池中的线程没有"核心/非核心"的身份标记,仅通过创建时机和 corePoolSize 区分:

  • 当线程数 ≤ corePoolSize 时,新创建的线程视为核心线程;当线程数 > corePoolSize 时,新创建的视为非核心线程。

  • 即使核心线程被回收(如开启超时回收),线程池也会重新创建新的核心线程来补足 corePoolSize 的数量,不会将已有的非核心线程"升级"为核心线程。

相关推荐
左左右右左右摇晃2 小时前
Java并发——线程间的通信
java·开发语言
用户298698530142 小时前
Java: 从 Word 文档中提取文本和图像
java·后端
皙然2 小时前
吃透进程与线程:从概念到实战,破解并发编程核心难题
java·开发语言
冬夜戏雪2 小时前
HashMAP底层原理和扰动hash的例子
java·开发语言
咸鱼2.02 小时前
【java入门到放弃】计算机网络
java·开发语言·计算机网络
Zzxy2 小时前
MyBatis-Plus入门
java·mybatis
木井巳2 小时前
【递归算法】找出所有子集的异或总和再求和
java·算法·leetcode·决策树·深度优先
悟空码字2 小时前
【保姆级】实现APP分享至微信,看完就能落地
java·后端·微信
常利兵2 小时前
Android 开发探秘:View.post()为何能获取View宽高
java·数据库·sql