一、基础核心类与工作原理
1. 线程池的核心工作原理是什么?(高频必问)
核心是「复用线程、控制并发量」,避免频繁创建/销毁线程的性能开销,执行流程分4步:
-
提交任务后,先判断核心线程数(corePoolSize)是否已满:未满则创建核心线程执行任务;已满则将任务加入阻塞队列。
-
若阻塞队列也已满,判断最大线程数(maximumPoolSize)是否已满:未满则创建非核心线程执行任务;已满则执行拒绝策略。
-
非核心线程在空闲超过保活时间(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 的数量,不会将已有的非核心线程"升级"为核心线程。