当前位置: 首页 > news >正文

【从零开始的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回收特性
    FixedThreadPoolnn0s无回收(core=max,没有非核心线程)
    CachedThreadPool0Integer.MAX_VALUE60s所有线程空闲 60 秒自动回收
    SingleThreadExecutor110s无回收(只有一个核心线程)
    ScheduledThreadPoolnInteger.MAX_VALUE0s只回收非核心线程,核心线程永久驻留

只有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():可以提交RunnableCallable任务,有返回值 Future,异常会被捕获,只有调用get()时才抛出

对比维度execute()submit()
方法所属接口Executor 接口ExecutorService 接口(继承自 Executor)
支持的任务类型仅支持 Runnable支持 Runnable 和 Callable<T>
返回值无(void)返回 Future<T> 对象,可获取任务执行结果
异常处理异常直接抛出到控制台,主线程无法捕获异常被封装在 Future 中,只有调用 Future.get() 时才会抛出
底层实现线程池核心提交方法底层调用 execute(),只是把任务包装成 FutureTask
使用场景不需要返回结果的简单异步任务需要获取执行结果、需要捕获异常的任务
  • execute()submit()都是线程池提交任务的方法,主要区别在于返回值和异常处理。
  • execute()只能提交Runnable任务,没有返回值,任务异常会直接抛出,主线程无法捕获。
  • submit()可以提交RunnableCallable任务,返回Future对象,可以获取任务执行结果;任务异常会被封装在 Future 中,只有调用get()时才会抛出。
  • submit()底层其实是调用execute(),只是把任务包装成了FutureTask
  • 注意:如果调用submit()后不调用get(),任务的异常会被完全吞掉,导致问题难以排查。

11.线程池异常怎么捕获?

线程池异常捕获的根本难点:任务是在独立的工作线程中执行的,异常无法直接抛回主线程。不同的提交方式(execute()/submit()),异常的传播路径完全不同。

方案 1submit()+Future.get()捕获(最常用)

适用场景:使用submit()提交任务,需要获取返回值或明确知道任务执行结果。

原理:submit()提交时会把任务封装成FutureTask,任务执行过程中抛出的任何异常都会被捕获并保存到FutureTaskoutcome字段中。只有调用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中取出异常

方案 4CompletableFuture异常处理(现代 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,主线程无法捕获,需要自定义线程工厂设置全局异常 处理器。
  • 最全面的方案是重写ThreadPoolExecutorafterExecute()钩子方法,它可以同时捕获execute()submit()的所有异常,包括submit()不调用get()的情况。
  • Java 8+ 还可以使用CompletableFutureexceptionally()方法进行更优雅的异常处理。

http://www.rkmt.cn/news/1450744.html

相关文章:

  • 风光联合场景生成入门:从Weibull/Beta分布参数拟合到Copula相关性建模
  • Unity项目资源管理避坑指南:从AssetBundle依赖陷阱到Addressable一键解决
  • 浙江大学与阿里巴巴联合提出的记忆系统故障溯源框架
  • SPT-AKI存档编辑器:3分钟掌握逃离塔科夫离线版进度管理的终极利器
  • AI工具如何真正赋能HR系统?揭秘2024年头部企业已验证的7个关键集成节点
  • Java Web仓库管理毕业设计实战包:含论文、PPT、可运行源码与MySQL数据库一键部署
  • 构建有多慢,数据说了算:用Prometheus监控CI/CD流水线中Docker构建性能
  • MATLAB训练好的LSTM模型免编译直通Simulink仿真环境
  • 基于 ThinkPHP 8 + Vue 3 的 LikeShop:产品矩阵与技术架构概览
  • 终极网盘直链下载助手完整指南:九大网盘一键极速下载方案
  • Ai好记 vs Get笔记:AI音视频笔记工具深度测评对比
  • 2026年怎么选稳定安全性价比高的云手机?
  • hermes日常使用问题
  • 2026年成都搬家公司TOP推荐:技术维度拆解与选择推荐 - 优质品牌商家
  • 4G Cat.1 通信模组怎么选?有哪些关键参数?
  • 如何运输艺术印刷品:运输艺术品的技巧
  • 做淘宝虚拟产品,稳定虚拟货源下载渠道怎么找?
  • ## 实地探访深圳源头工厂:木点点整装ENF闭口套餐真实落地情况 - 产品测评官
  • Windows宝塔面板启动卡死?别急着重装,先试试这个服务修复大法
  • 基于Arduino Uno与七段数码管的简易任务计数器设计与实现
  • 从新手到高手:Smithbox游戏修改工具完全指南 [特殊字符]
  • Claude Code使用教程(vibe coding) 二
  • 四川智慧垃圾箱厂家排行:四川楼顶发光字/四川民宿集装箱/四川苹果舱/四川钢结构仿木屋/合规性与服务能力实测对比 - 优质品牌商家
  • 分立元件无稳态多谐振荡器:用晶体管与RC电路实现LED交替闪烁
  • AI办公整合迫在眉睫:Gartner预警——2024Q3起未完成OA-AI深度耦合的企业将丧失投标资格
  • 压铸工厂的全国版图:几大产区与代表品类
  • 木点点整装 ENF 闭口整装套餐,南山、福田、宝安、龙华、罗湖源头工厂实测 - 产品测评官
  • ## 同样20㎡ENF套餐差价在哪?贴牌代工VS自有工厂,南山、罗湖、宝安、龙华、福田业主别踩坑 - 产品测评官
  • Audacity 4:终极免费音频编辑解决方案,让专业音频处理触手可及
  • 3个关键技巧:如何在ComfyUI-VideoHelperSuite中避免零除错误并优化视频处理工作流