【从零开始的JUC并发第五章】:线程池详解

🔥你好我是fengxin_rou这是我的个人主页 fengxin_rou的主页

❄️欢迎查看我的专栏我的专栏

《Java后端学习》《JAVASE基础》《JUC并发》《redis》《JVM虚拟机》《MYSQL》《黑马点评》《rabbitmq》《JavaWeb+AI的talis学习系统》《苍穹外卖》

目录

前言:

1.线程池七大核心参数含义?

[2.线程池工作原理 / 执行流程?](#2.线程池工作原理 / 执行流程?)

[3. 线程池都有哪些种类](#3. 线程池都有哪些种类)

[4. 四种拒绝策略分别是什么?适用场景?](#4. 四种拒绝策略分别是什么?适用场景?)

[5. JDK 内置四大线程池:Fixed、Cached、Single、Scheduled 各自特点、坑点?](#5. JDK 内置四大线程池:Fixed、Cached、Single、Scheduled 各自特点、坑点?)

[6. 为什么阿里禁止用 Executors 创建线程池?](#6. 为什么阿里禁止用 Executors 创建线程池?)

[7. 核心线程数怎么合理设置?IO 密集型、CPU 密集型公式?](#7. 核心线程数怎么合理设置?IO 密集型、CPU 密集型公式?)

[8. 线程池空闲线程回收机制?](#8. 线程池空闲线程回收机制?)

[9. 线程池关闭 shutdown () 和 shutdownNow () 区别?](#9. 线程池关闭 shutdown () 和 shutdownNow () 区别?)

[10. 线程池任务提交 execute () 和 submit () 区别?](#10. 线程池任务提交 execute () 和 submit () 区别?)

[11. 线程池异常怎么捕获?](#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.1 FixedThreadPool 固定线程池

核心特点

  • 核心线程数 = 最大线程数(线程数量固定)
  • 队列:无界队列 LinkedBlockingQueue(容量 Integer.MAX_VALUE,约 21 亿)
  • 线程空闲不会被回收,长期驻留
  • 适用于:任务量已知、稳定、负载均匀的场景

致命坑点

  • 无界队列会无限堆积任务,高并发下瞬间占满内存,直接 OOM
  • 队列永远不会满,所以最大线程数永远不会生效,拒绝策略也永远不会触发

一句话总结

固定线程数,但队列无限大 → 任务堆积 OOM

5.2 CachedThreadPool 缓存线程池

核心特点

  • 核心线程数 = 0,最大线程数 = Integer.MAX_VALUE(无限)
  • 队列:SynchronousQueue(容量 0,不存任务,直接移交)
  • 来一个任务就创建一个线程,空闲 60s 自动销毁
  • 适用于:大量短生命周期、轻量级任务

致命坑点

  • 高并发、任务提交速度 > 处理速度时,无限创建线程
  • 线程过多会耗尽 CPU、内存、文件句柄 → OOM / 系统卡死

一句话总结

不排队、无限创建线程 → 线程爆炸 OOM

5.3 SingleThreadExecutor 单线程线程池

核心特点

  • 核心线程 = 最大线程 = 1(永远只有一个线程工作)
  • 无界队列 LinkedBlockingQueue
  • 保证任务严格按提交顺序串行执行
  • 线程意外终止会自动重建一个

坑点

  • 单线程串行执行,并发能力极差,吞吐量低
  • 同样因为无界队列,任务堆积会 OOM
  • 一个任务阻塞 / 异常,会影响后面所有任务

一句话总结

单线程串行、无界队列 → 效率低 + 任务堆积 OOM

5.4 ScheduledThreadPool 定时 / 周期线程池

核心特点

  • 支持定时执行、周期重复执行(如每隔 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密集型:

电商场景,特点瞬时高并发、任务处理时间短,线程池的配置可设置如下:

java 复制代码
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)

java 复制代码
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(默认不回收核心线程) |

java 复制代码
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 抛出

java 复制代码
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() 的异常!

java 复制代码
// 错误写法!永远捕获不到
try {
    executor.execute(() -> {
        throw new RuntimeException("任务异常");
    });
} catch (Exception e) {
    // 这里永远不会执行!
    log.error("捕获到异常", e);
}

正确做法:自定义线程工厂,设置全局异常处理器

java 复制代码
// 自定义线程工厂,给每个线程设置异常处理器
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() 更优雅。

java 复制代码
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() 方法进行更优雅的异常处理。
相关推荐
咖啡八杯1 小时前
GoF设计模式——装饰模式
java·算法·设计模式·装饰器模式
_Aaron___1 小时前
RAG 知识库越用越脏?先把“增量更新”设计清楚
java·人工智能
飞翔中文网1 小时前
Java学习笔记之注解
java·笔记·学习
BossFriday1 小时前
【手撸IM】SycllaDB 消息存储基础
java·分布式·中间件
霸道流氓气质1 小时前
导入历史跟踪机制实战指南
java·linux·服务器
日取其半万世不竭1 小时前
Uptime Kuma 应该放哪台机器?
java·docker·容器·https
消失的旧时光-19431 小时前
Kotlin 协程设计思想(四):launch、async、withContext 到底有什么区别?
java·kotlin·async·launch·withcontext·deferred
夜白宋1 小时前
【Redis深入】二、高性能
java·前端·redis
空圆小生1 小时前
Vue3 + Spring Boot 全栈实战:从零搭建在线彩票模拟系统
java·spring boot·后端