【从零开始的JUC并发第五章】:线程池详解
🔥你好我是fengxin_rou这是我的个人主页fengxin_rou的主页
❄️欢迎查看我的专栏我的专栏
《Java后端学习》、《JAVASE基础》、《JUC并发》、《redis》、《JVM虚拟机》、《MYSQL》、《黑马点评》、《rabbitmq》、《JavaWeb+AI的talis学习系统》、《苍穹外卖》
目录
前言:
1.线程池七大核心参数含义?
2.线程池工作原理 / 执行流程?
3. 线程池都有哪些种类
4. 四种拒绝策略分别是什么?适用场景?
5. JDK 内置四大线程池:Fixed、Cached、Single、Scheduled 各自特点、坑点?
6. 为什么阿里禁止用 Executors 创建线程池?
7. 核心线程数怎么合理设置?IO 密集型、CPU 密集型公式?
8. 线程池空闲线程回收机制?
9. 线程池关闭 shutdown () 和 shutdownNow () 区别?
10. 线程池任务提交 execute () 和 submit () 区别?
11. 线程池异常怎么捕获?
前言:
本文介绍了JUC并发种线程池相关属性、以及线程池相关种类、回收机制等面试常考问题
1.线程池七大核心参数含义?
corePoolSize:核心线程容量大小,线程池中线程的数量如果 <= corePoolSize,那么即使这些线程处于空闲状态,也不会被销毁。
maximumPoolSize:最大线程容量大小,限制了线程池能创建的最大线程总数(包括核心线程和非核心线程),当阻塞队列也就是workQueue已满,并且数量最大线程总数以内的话,会创建新线程来处理任务。如果两者都满了,则会触发handler拒绝策略
keepAliveTime:超过corePoolSize数量的线程在空闲状态能存活的时长
unit:keepAliveTime的时间单位
workQueue:工作队列,当没有空闲的线程执行新任务时,该任务就会被放入工作队列中,等待执行。
threadFactory:线程工作工厂,可以用来修改线程名字
handler:拒绝策略,当线程数已达maximumPoolSize且工作队列已满时,对新提交任务的处理策略(如直接抛出异常、由提交任务的线程执行等)
2.线程池工作原理 / 执行流程?
首先提交任务,提交任务之后判断线程数量是否小于核心线程数,若小于则直接执行任务,若大于则进入阻塞队列,阻塞队列若没满则等待空闲线程执行任务,满了就需要创建非核心线程来执行任务,若线程池满了就触发拒绝策略
3.线程池都有哪些种类
ScheduledThreadPool:可以设置定期的执行任务,它支持周期性或定时任务,比如每隔10秒执行一次任务
FixedThreadPool:它的核心线程数即最大线程数,它的特点是线程池中的线程数除了初始阶段需要从 0 开始增加外,之后的线程数量就是固定的。当任务数超过核心线程数,不会创建新的线程执行任务,而是
把线程加入以LinkedBlockingQueue为底层的workQueue,它的特点是无界(设置的Integer.MAX_VALUE),当消费速度跟不上生产速度时,会导致消息堆积,最终导致发生OOM,这是阿里手册禁用Executors.newFixedThreadPool()的主要原因
CachedThreadPool:又称缓存线程池,特点在于理论上没有线程池容量限制(maximumPoolSize = Integer.MAX_VALUE),当线程限制60秒后会被回收。底层以SychronousQueue为workQueue,特点是没有容量,直接中转或传递任务,每来一个新的任务就会创建一个新的线程来执行。所以在高并发瞬时大量任务的情况下,CachedThreadPool会创建上千个线程,这样很可能把系统资源耗尽导致OOM,这也是阿里手册禁用的原因,实际情况请手动new ThreadPool并设计最大容量
SingleThreadExecutor:该线程池只有一个线程,并且只使用这唯一的线程去执行任务,如果线程池仅有的一个线程在执行任务中发生异常,那么线程池会创建一个新的线程去执行这剩下的任务。因为只有一个线程,所以适合需要按被提交顺序去依次执行的场景。而前面的不行,因为前面的线程有多个并且并行执行
SingleThreadScheduledExecutor:也就是ScheduledThreadPool的单一线程版,只有一个线程能用其余特性和ScheduledThreadExecutor一样
4.四种拒绝策略分别是什么?适用场景?
分别是callerPolicy、abortPolicy、discardPolicy、discardOldestPolicy
callerPolicy:让调用这个任务的线程去执行这个被拒绝的任务,除非线程池停止或者线程池的任务队列已有空缺
abortPolicy:直接抛出任务被线程池拒绝的异常
discardPolicy:不做任何处理,静默拒绝任务
discardOldestPolicy:抛弃最老的任务,来执行当前任务
5.JDK 内置四大线程池:Fixed、Cached、Single、Scheduled 各自特点、坑点?
5.1FixedThreadPool 固定线程池
核心特点
- 核心线程数 = 最大线程数(线程数量固定)
- 队列:无界队列LinkedBlockingQueue(容量 Integer.MAX_VALUE,约 21 亿)
- 线程空闲不会被回收,长期驻留
- 适用于:任务量已知、稳定、负载均匀的场景
致命坑点
- 无界队列会无限堆积任务,高并发下瞬间占满内存,直接 OOM
- 队列永远不会满,所以最大线程数永远不会生效,拒绝策略也永远不会触发
一句话总结
固定线程数,但队列无限大 → 任务堆积 OOM
5.2CachedThreadPool 缓存线程池
核心特点
- 核心线程数 = 0,最大线程数 = Integer.MAX_VALUE(无限)
- 队列:SynchronousQueue(容量 0,不存任务,直接移交)
- 来一个任务就创建一个线程,空闲 60s 自动销毁
- 适用于:大量短生命周期、轻量级任务
致命坑点
- 高并发、任务提交速度 > 处理速度时,无限创建线程
- 线程过多会耗尽 CPU、内存、文件句柄 → OOM / 系统卡死
一句话总结
不排队、无限创建线程 → 线程爆炸 OOM
5.3SingleThreadExecutor 单线程线程池
核心特点
- 核心线程 = 最大线程 = 1(永远只有一个线程工作)
- 无界队列LinkedBlockingQueue
- 保证任务严格按提交顺序串行执行
- 线程意外终止会自动重建一个
坑点
- 单线程串行执行,并发能力极差,吞吐量低
- 同样因为无界队列,任务堆积会 OOM
- 一个任务阻塞 / 异常,会影响后面所有任务
一句话总结
单线程串行、无界队列 → 效率低 + 任务堆积 OOM
5.4ScheduledThreadPool 定时 / 周期线程池
核心特点
- 支持定时执行、周期重复执行(如每隔 5 秒执行一次)
- 队列:DelayedWorkQueue延迟队列
- SingleThreadScheduledExecutor:单线程版本
坑点
- 周期任务抛出异常且未捕获,会直接停止调度,不再执行
- 任务执行时间 > 周期间隔时,不会并发执行,会延迟执行
- 同样无界队列,任务过多会 OOM
- 单线程版本:效率极低,一个任务阻塞全部卡住
一句话总结
定时任务专用,但异常会中断调度 + 无界队列风险
| 线程池 | 核心特点 | 最大坑点 |
| Fixed | 固定线程数,无界队列 | 任务堆积 → OOM |
| Cached | 无限线程,不排队 | 线程爆炸 → OOM |
| Single | 单线程串行 | 效率低 + 堆积 OOM |
| Scheduled | 定时 / 周期执行 | 异常中断调度 + 延迟执行 |
6.为什么阿里禁止用 Executors 创建线程池?
直接使用Executor创建线程池,会导致队列、线程池最大容量没有设计上限,在高并发场景下会耗尽服务器资源,直接引发内存溢出(OOM)。
考虑到两个OOM情况:
线程爆炸:在executor创建Cached线程池的时候,会因无限容器大小且高并发的情况下,创建过多线程导致耗空系统资源OOM的情况,拒绝策略永远不会触发,最大线程数形同虚设
任务堆积:在创建executor创建Fixed或Scheduled的时候,会因为有固定大小的容量,且无限大小的队列(LinkedBlockingQueue/DelayedWorkQueue),会导致消息堆积,导致OOM,并且周期任务抛异常会直接停止调度,无容错
无法自定义线程名:
- Executors创建的线程名默认是pool-1-thread-1
- 出问题无法快速定位业务代码,排查困难
7.核心线程数怎么合理设置?IO 密集型、CPU 密集型公式?
线程池线程数要根据任务类型设置,分为 CPU 密集型 和 IO 密集型。
CPU密集型公式:corePoolSize = CPU核数 + 1
- 特点:纯计算,无 IO 等待
- 目的:减少线程上下文切换,让 CPU 跑满
- 为什么 + 1:防止线程偶尔阻塞,仍能榨干 CPU
IO密集型公式:corePoolSize = CPU核数 * 2
- 特点:大量等待 IO,CPU 空闲时间多
- 目的:CPU 等待时,用其他线程继续干活
- 生产最常用、最安全:CPU × 2
场景一IO密集型:
电商场景,特点瞬时高并发、任务处理时间短,线程池的配置可设置如下:
new ThreadPoolExecutor( 16, // corePoolSize = 16(假设8核CPU × 2) 32, // maximumPoolSize = 32(突发流量扩容) 10, TimeUnit.SECONDS, // 非核心线程空闲10秒回收 new SynchronousQueue<>(), // 不缓存任务, 直接扩容线程 new AbortPolicy() // 直接拒绝, 避免系统过载 );说明:
使用SynchronousQueue确保任务直达线程,避免队列延迟。
拒绝策略快速失败,前端返回“活动火爆”提示,结合降级策略(如缓存预热)。
场景二:CPU 密集型场景(纯计算任务)
场景描述
- 视频帧处理、图片滤镜、加密解密、大数据排序、复杂公式计算
- 无 IO、无等待、纯吃 CPU
线程池配置(8 核 CPU)
new ThreadPoolExecutor( 9, // corePoolSize = CPU 核心数 + 1 → 8+1=9 9, // maximumPoolSize = 和核心线程一样(固定线程) 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(100), // 有界队列,少量排队 new ThreadPoolExecutor.CallerRunsPolicy() // 过载让调用者执行,不丢任务 );8.线程池空闲线程回收机制?
总结:当一个线程空闲时间超过 keepAliveTime,并且当前线程数 > 核心线程数(或允许核心线程超时),就会被回收,只在特定情况回收核心线程。
主要由两个参数keepAliveTime和allowCoreThreadTimeOut和一个方法getTask()控制
| 参数 | 作用 | 默认值 |
| keepAliveTime | 线程的最大空闲时间,超过这个时间就会被回收 | 无 |
| allowCoreThreadTimeOut | 是否允许核心线程被回收 | false(默认不回收核心线程) |
while (true) { // 1. 判断是否需要超时等待 boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; // 2. 从队列取任务: // - timed=true:超时等待 keepAliveTime 时间 // - timed=false:无限阻塞等待(核心线程默认) Runnable task = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take(); // 3. 超时没拿到任务 → 返回 null → 线程被回收 if (task != null) return task; }可知getTask()是获取任务的方法,如果在超时的情况下没有拿到线程,则会被回收线程
工作线程的生命周期
- 线程被创建后,进入循环,不断调用getTask()从队列取任务
- 拿到任务 → 执行run()方法
- 执行完任务 → 回到循环,再次调用getTask()
- 如果getTask()返回null→ 线程退出,被回收
线程池 核心线程数 最大线程数 keepAliveTime 回收特性 FixedThreadPool n n 0s 无回收(core=max,没有非核心线程) CachedThreadPool 0 Integer.MAX_VALUE 60s 所有线程空闲 60 秒自动回收 SingleThreadExecutor 1 1 0s 无回收(只有一个核心线程) ScheduledThreadPool n Integer.MAX_VALUE 0s 只回收非核心线程,核心线程永久驻留
只有Cached线程池由默认值回收时间60s
9.线程池关闭 shutdown () 和 shutdownNow () 区别?
| 对比维度 | shutdown() | shutdownNow() |
| 线程池状态 | 变为 SHUTDOWN | 变为 STOP |
| 已提交正在执行的任务 | 继续执行,直到完成 | 尝试中断(仅设置中断标志位) |
| 队列中等待的任务 | 全部执行 | 全部丢弃,不执行 |
| 返回值 | 无(void) | 返回队列中未执行的任务列表 List<Runnable> |
| 能否提交新任务 | ❌ 不能,抛出 RejectedExecutionException | ❌ 不能,抛出 RejectedExecutionException |
| 中断对象 | 仅中断空闲线程 | 中断所有线程(包括正在执行的) |
| 关闭速度 | 慢,等待所有任务完成 | 快,立即停止大部分任务 |
| 任务丢失 | 无 | 丢失队列中所有等待的任务 |
- shutdown()和shutdownNow()是线程池关闭的两个核心方法,主要区别在于对任务的处理方式。
- shutdown()是温柔关闭:设置状态为 SHUTDOWN,中断空闲线程,等待所有已提交任务(包括队列中的)执行完成后关闭,不丢失任务。
- shutdownNow()是暴力关闭:设置状态为 STOP,中断所有线程,清空队列并返回未执行的任务列表,会放弃执行队列中的未执行任务。
- 最重要的一点:shutdownNow()只是设置中断标志位,如果任务不响应中断,线程会继续运行。
- 生产环境推荐使用优雅关闭流程:先调用shutdown()等待,超时再调用shutdownNow()。
10.线程池任务提交 execute () 和 submit () 区别?
execute():只能提交Runnable任务,无返回值,异常直接抛出
submit():可以提交Runnable和Callable任务,有返回值 Future,异常会被捕获,只有调用get()时才抛出
| 对比维度 | execute() | submit() |
| 方法所属接口 | Executor 接口 | ExecutorService 接口(继承自 Executor) |
| 支持的任务类型 | 仅支持 Runnable | 支持 Runnable 和 Callable<T> |
| 返回值 | 无(void) | 返回 Future<T> 对象,可获取任务执行结果 |
| 异常处理 | 异常直接抛出到控制台,主线程无法捕获 | 异常被封装在 Future 中,只有调用 Future.get() 时才会抛出 |
| 底层实现 | 线程池核心提交方法 | 底层调用 execute(),只是把任务包装成 FutureTask |
| 使用场景 | 不需要返回结果的简单异步任务 | 需要获取执行结果、需要捕获异常的任务 |
- execute()和submit()都是线程池提交任务的方法,主要区别在于返回值和异常处理。
- execute()只能提交Runnable任务,没有返回值,任务异常会直接抛出,主线程无法捕获。
- submit()可以提交Runnable和Callable任务,返回Future对象,可以获取任务执行结果;任务异常会被封装在 Future 中,只有调用get()时才会抛出。
- submit()底层其实是调用execute(),只是把任务包装成了FutureTask。
- 注意:如果调用submit()后不调用get(),任务的异常会被完全吞掉,导致问题难以排查。
11.线程池异常怎么捕获?
线程池异常捕获的根本难点:任务是在独立的工作线程中执行的,异常无法直接抛回主线程。不同的提交方式(execute()/submit()),异常的传播路径完全不同。
方案 1:submit()+Future.get()捕获(最常用)
适用场景:使用submit()提交任务,需要获取返回值或明确知道任务执行结果。
原理:submit()提交时会把任务封装成FutureTask,任务执行过程中抛出的任何异常都会被捕获并保存到FutureTask的outcome字段中。只有调用get()方法时,才会把异常包装成ExecutionException抛出
Future<Integer> future = executor.submit(() -> { // 可能抛出异常的任务 return 1 / 0; }); try { Integer result = future.get(); // 这里才会抛出异常 } catch (ExecutionException e) { // 捕获任务抛出的异常 Throwable cause = e.getCause(); // 获取原始异常 log.error("任务执行失败", cause); } catch (InterruptedException e) { // 捕获等待过程中被中断的异常 Thread.currentThread().interrupt(); log.error("等待任务结果被中断", e); }注意:如果调用submit()后不调用get(),异常会被完全吞掉! 没有任何日志,没有任何提示,你永远不知道任务执行失败了。
方案 2:自定义UncaughtExceptionHandler捕获execute()异常
适用场景:使用execute()提交任务,不需要返回值。
原理:
execute()提交的任务抛出异常时,不会被线程池捕获,会直接向上抛到线程的UncaughtExceptionHandler。如果没有自定义处理器,默认会打印到控制台。
注意:主线程的try-catch永远捕获不到execute()的异常!
// 错误写法!永远捕获不到 try { executor.execute(() -> { throw new RuntimeException("任务异常"); }); } catch (Exception e) { // 这里永远不会执行! log.error("捕获到异常", e); }正确做法:自定义线程工厂,设置全局异常处理器
// 自定义线程工厂,给每个线程设置异常处理器 ThreadFactory threadFactory = r -> { Thread thread = new Thread(r); thread.setName("my-thread-pool-%d"); // 设置未捕获异常处理器 thread.setUncaughtExceptionHandler((t, e) -> { log.error("线程 {} 发生未捕获异常", t.getName(), e); }); return thread; }; // 创建线程池时使用自定义线程工厂 ExecutorService executor = new ThreadPoolExecutor( 8, 16, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100), threadFactory, // 关键! new AbortPolicy() );方案 3:重写ThreadPoolExecutor.afterExecute()方法(最全面)
适用场景:生产环境推荐,同时捕获execute()和submit()的所有异常,包括submit()不调用get()的情况。
原理:
ThreadPoolExecutor提供了钩子方法afterExecute(Runnable r, Throwable t),每个任务执行完成后都会调用这个方法。
- 如果是execute()提交的任务,异常会直接通过t参数传入
- 如果是submit()提交的任务,t参数为null,需要从Future中取出异常
方案 4:CompletableFuture异常处理(现代 Java 推荐)
适用场景:使用CompletableFuture进行异步编程(Java 8+)。
原理:
CompletableFuture提供了专门的异常处理方法exceptionally()和handle(),比传统的Future.get()更优雅。
CompletableFuture.supplyAsync(() -> { // 任务逻辑 return 1 / 0; }, executor) .exceptionally(e -> { // 捕获异常,返回默认值 log.error("任务执行失败", e); return 0; }) .thenAccept(result -> { // 处理正常结果 System.out.println("结果:" + result); });| 捕获方案 | 适用提交方式 | 优点 | 缺点 |
| submit() + Future.get() | 仅 submit() | 简单直接,能获取返回值 | 不调用 get() 异常被吞 |
| UncaughtExceptionHandler | 仅 execute() | 全局统一处理 execute() 异常 | 无法处理 submit() 异常 |
| 重写 afterExecute() | execute() + submit() | 最全面,所有异常都能捕获 | 需要自定义线程池 |
| CompletableFuture 异常处理 | CompletableFuture | 链式调用,优雅灵活 | 仅适用于 CompletableFuture |
面试回答(直接背)
- 线程池异常捕获主要有 4 种方案,不同提交方式的异常传播路径不同。
- 对于submit()提交的任务,异常会被封装在Future对象中,只有调用get()方法时才会抛出ExecutionException;如果不调用get(),异常会被完全吞掉。
- 对于execute()提交的任务,异常会直接抛到线程的UncaughtExceptionHandler,主线程无法捕获,需要自定义线程工厂设置全局异常 处理器。
- 最全面的方案是重写ThreadPoolExecutor的afterExecute()钩子方法,它可以同时捕获execute()和submit()的所有异常,包括submit()不调用get()的情况。
- Java 8+ 还可以使用CompletableFuture的exceptionally()方法进行更优雅的异常处理。
