线程池面试

下面我用一个生活类比先帮你建立感觉:线程池就像一个"外卖骑手站点"

如果每来一个订单,就临时招聘一个骑手,送完就解雇,这很浪费。线程也是一样:如果每来一个任务,就 new Thread(),任务结束线程销毁,会有很大的创建和销毁成本。

所以线程池的思想是:

先准备好一批线程,任务来了就交给这些线程执行,执行完线程不销毁,而是继续等下一个任务。


1. 什么是线程池?

线程池就是提前创建并管理一批线程的工具。

在 Java 里,线程池通常由 ThreadPoolExecutor 实现。

你可以简单理解为:

java 复制代码
任务来了 -> 放进线程池 -> 线程池安排线程执行

例如不用线程池时:

java 复制代码
new Thread(() -> {
    System.out.println("执行任务");
}).start();

问题是:每次都创建新线程。

使用线程池后:

java 复制代码
ExecutorService executor = Executors.newFixedThreadPool(5);

executor.execute(() -> {
    System.out.println("执行任务");
});

意思是:我创建一个有 5 个线程的线程池,以后任务都交给它。


2. 为什么要用线程池?

核心原因有 3 个。


① 减少线程创建和销毁的开销

线程不是普通对象,它对应操作系统里的线程资源。

频繁这样写:

java 复制代码
new Thread(task).start();

会导致:

text 复制代码
创建线程 -> 执行任务 -> 销毁线程
创建线程 -> 执行任务 -> 销毁线程
创建线程 -> 执行任务 -> 销毁线程

如果请求很多,就很浪费。

线程池可以复用线程:

text 复制代码
线程1:任务A -> 任务B -> 任务C
线程2:任务D -> 任务E -> 任务F

② 控制并发数量,防止系统被压垮

假设你的网站来了 10000 个请求,如果你直接给每个请求创建一个线程,可能会出现:

text 复制代码
线程太多
CPU频繁切换
内存暴涨
系统卡死
甚至 OOM

线程池可以限制最多同时有多少线程执行。

比如设置最大线程数是 20,那么即使来了 10000 个任务,也不会无限创建线程。


③ 统一管理任务

线程池可以做到:

text 复制代码
统一提交任务
统一控制线程数量
统一处理队列
统一拒绝策略
统一关闭线程池

比如你可以控制:

text 复制代码
最多几个线程?
任务满了怎么办?
线程空闲多久销毁?
任务放哪个队列?
线程名字叫什么?

这比到处 new Thread() 更可控。


3. 怎么用线程池?

方式一:使用 Executors,简单但不推荐面试中说它是最佳方式

例如:

java 复制代码
ExecutorService executor = Executors.newFixedThreadPool(5);

executor.execute(() -> {
    System.out.println("执行任务");
});

executor.shutdown();

这表示创建一个固定 5 个线程的线程池。

但是面试里一般会问:

你平时怎么创建线程池?

最好回答:

一般不直接使用 Executors 创建线程池,而是使用 ThreadPoolExecutor 手动指定核心参数,因为 Executors 的一些默认队列可能是无界队列,存在 OOM 风险。


方式二:使用 ThreadPoolExecutor,推荐

java 复制代码
import java.util.concurrent.*;

public class ThreadPoolDemo {
    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2,                      // corePoolSize 核心线程数
                5,                      // maximumPoolSize 最大线程数
                60,                     // keepAliveTime 空闲线程存活时间
                TimeUnit.SECONDS,       // 时间单位
                new ArrayBlockingQueue<>(10), // workQueue 任务队列
                Executors.defaultThreadFactory(), // threadFactory 线程工厂
                new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
        );

        for (int i = 0; i < 20; i++) {
            int taskId = i;
            executor.execute(() -> {
                System.out.println(Thread.currentThread().getName() + " 执行任务:" + taskId);
            });
        }

        executor.shutdown();
    }
}

这才是比较标准的写法。


4. 线程池的参数有哪些?

ThreadPoolExecutor 最核心的构造方法是这个:

java 复制代码
public ThreadPoolExecutor(
    int corePoolSize,
    int maximumPoolSize,
    long keepAliveTime,
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue,
    ThreadFactory threadFactory,
    RejectedExecutionHandler handler
)

一共有 7 个核心参数。


参数 1:corePoolSize,核心线程数

核心线程数就是线程池长期保留的线程数量。

比如:

java 复制代码
corePoolSize = 5

意思是:线程池平时至少维持 5 个核心线程工作。

哪怕这些线程空闲了,一般也不会被销毁。

可以理解为外卖站点的正式骑手。


参数 2:maximumPoolSize,最大线程数

最大线程数表示线程池最多能创建多少个线程。

比如:

java 复制代码
maximumPoolSize = 10

意思是:线程池最多只能有 10 个线程。

可以理解为:

text 复制代码
5 个正式骑手 + 5 个临时骑手

当任务太多,核心线程忙不过来,队列也满了,就会创建临时线程,但最多不能超过 maximumPoolSize


参数 3:keepAliveTime,非核心线程空闲存活时间

非核心线程空闲多久后会被销毁。

比如:

java 复制代码
keepAliveTime = 60
TimeUnit.SECONDS

意思是:临时线程如果空闲超过 60 秒,就会被销毁。

注意,默认情况下主要影响的是非核心线程


参数 4:unit,时间单位

配合 keepAliveTime 使用。

常见有:

java 复制代码
TimeUnit.SECONDS
TimeUnit.MILLISECONDS
TimeUnit.MINUTES

例如:

java 复制代码
60, TimeUnit.SECONDS

表示 60 秒。


参数 5:workQueue,任务队列

任务队列用来存放还没来得及执行的任务。

比如线程都在忙,新来的任务可以先排队。

常见队列有:

① ArrayBlockingQueue

有界队列,需要指定大小。

java 复制代码
new ArrayBlockingQueue<>(100)

最多只能排 100 个任务。

面试和实际开发中比较推荐,因为它有边界,不容易无限堆积任务。


② LinkedBlockingQueue

链表队列。

如果不指定容量,默认容量非常大,容易堆积大量任务,造成 OOM。

java 复制代码
new LinkedBlockingQueue<>()

这种要谨慎。


③ SynchronousQueue

不存储任务,任务必须直接交给线程执行。

如果没有空闲线程,就尝试创建新线程。

Executors.newCachedThreadPool() 底层就用了它。


④ PriorityBlockingQueue

优先级队列,可以让任务按优先级执行。

不常用于普通业务。


参数 6:threadFactory,线程工厂

线程工厂用来创建线程。

它可以设置线程名字、是否为守护线程等。

守护线程是什么

Java 线程分两种:

c 复制代码
普通线程:用户线程,也叫非守护线程
守护线程:daemon thread

守护线程的特点是:

它是"陪跑线程",专门服务普通线程。只要所有普通线程都结束了,JVM 就会退出,不会等守护线程执行完。

默认写法:

java 复制代码
Executors.defaultThreadFactory()

但是实际项目里最好自定义线程名,方便排查问题。

例如:

java 复制代码
ThreadFactory threadFactory = r -> {
    Thread thread = new Thread(r);
    thread.setName("order-thread-" + thread.getId());
    return thread;
};

这样日志里就能看到:

text 复制代码
order-thread-21
order-thread-22

比默认的:

text 复制代码
pool-1-thread-1

更容易定位问题。


参数 7:handler,拒绝策略

当线程池满了,队列也满了,新任务进不来时,就会触发拒绝策略。

常见有 4 种。


① AbortPolicy:直接抛异常

java 复制代码
new ThreadPoolExecutor.AbortPolicy()

这是默认策略。

任务提交失败,直接抛出 RejectedExecutionException

适合希望明确知道任务失败的场景。


② CallerRunsPolicy:谁提交,谁执行

java 复制代码
new ThreadPoolExecutor.CallerRunsPolicy()

意思是:线程池处理不了了,那就让提交任务的线程自己执行。

比如主线程提交任务,如果线程池满了,就由主线程自己执行。

优点是可以降低任务提交速度,起到一定的削峰作用。


③ DiscardPolicy:直接丢弃任务,不报错

java 复制代码
new ThreadPoolExecutor.DiscardPolicy()

任务直接被丢掉,也不抛异常。

一般要谨慎,因为任务可能悄悄丢失。


④ DiscardOldestPolicy:丢弃队列中最老的任务

java 复制代码
new ThreadPoolExecutor.DiscardOldestPolicy()

丢掉队列里等待最久的任务,然后尝试提交新任务。

也要谨慎使用。


重点:线程池的执行流程

这个是面试最喜欢问的。

假设参数如下:

java 复制代码
corePoolSize = 2
maximumPoolSize = 5
workQueue = 10

当任务来了,执行流程是:

text 复制代码
1. 当前线程数 < 核心线程数
   -> 创建核心线程执行任务

2. 核心线程都忙了
   -> 新任务进入任务队列排队

3. 队列也满了,并且当前线程数 < 最大线程数
   -> 创建非核心线程执行任务

4. 当前线程数已经达到最大线程数,并且队列也满了
   -> 执行拒绝策略

也就是:

text 复制代码
先用核心线程
再进队列
队列满了再扩容线程
线程也满了再拒绝

很多初学者会误以为:

text 复制代码
先创建到最大线程数,再放队列

这是错的。

Java 线程池默认流程是:

text 复制代码
核心线程 -> 队列 -> 最大线程 -> 拒绝策略

这个非常重要。


举个完整例子

假设:

java 复制代码
corePoolSize = 2
maximumPoolSize = 4
queueCapacity = 3

现在来了 8 个任务。

执行过程如下:

text 复制代码
任务1 -> 创建核心线程1执行
任务2 -> 创建核心线程2执行

任务3 -> 进入队列
任务4 -> 进入队列
任务5 -> 进入队列

任务6 -> 队列满了,创建非核心线程3执行
任务7 -> 队列满了,创建非核心线程4执行

任务8 -> 线程达到最大值4,队列也满了,触发拒绝策略

所以容量可以理解为:

text 复制代码
最大可承载任务数 = 最大线程数 + 队列容量

在这个例子里:

text 复制代码
4 + 3 = 7

第 8 个任务就会被拒绝。


5. 面试怎么问?怎么回答?

下面是常见面试题。


面试题 1:什么是线程池?

可以这样答:

线程池是一种线程复用机制。它会提前创建并维护一批线程,当任务提交过来时,不需要每次都创建新线程,而是把任务交给线程池中的线程执行。线程执行完任务后不会立即销毁,而是回到线程池中继续等待新的任务。这样可以降低线程创建和销毁的开销,并且可以统一控制并发数量。


面试题 2:为什么要用线程池?

可以这样答:

主要有三个原因。第一,线程的创建和销毁成本比较高,线程池可以复用线程,减少资源开销。第二,线程池可以限制并发线程数量,避免请求过多时无限创建线程导致 CPU 上下文切换过多或者内存溢出。第三,线程池可以统一管理任务,比如任务队列、拒绝策略、线程工厂、线程关闭等。


面试题 3:线程池有哪些核心参数?

可以这样答:

ThreadPoolExecutor 有 7 个核心参数:corePoolSize 核心线程数,maximumPoolSize 最大线程数,keepAliveTime 非核心线程空闲存活时间,unit 时间单位,workQueue 任务队列,threadFactory 线程工厂,handler 拒绝策略。

然后可以补一句:

其中最重要的是核心线程数、最大线程数、任务队列和拒绝策略。


面试题 4:线程池的执行流程是什么?

这是重点,可以这样答:

当一个任务提交到线程池时,如果当前线程数小于核心线程数,就创建核心线程执行任务;如果核心线程数已满,就把任务放入阻塞队列;如果队列也满了,并且当前线程数小于最大线程数,就创建非核心线程执行任务;如果线程数已经达到最大线程数并且队列也满了,就执行拒绝策略。

可以再简化成一句:

执行顺序是:核心线程、任务队列、非核心线程、拒绝策略。


面试题 5:线程池有哪些拒绝策略?

可以这样答:

Java 线程池内置了 4 种拒绝策略。AbortPolicy 是默认策略,会直接抛异常;CallerRunsPolicy 会让提交任务的线程自己执行任务;DiscardPolicy 会直接丢弃任务且不抛异常;DiscardOldestPolicy 会丢弃队列中最老的任务,然后尝试提交当前任务。


面试题 6:为什么不建议使用 Executors 创建线程池?

可以这样答:

因为 Executors 提供的一些快捷方法底层参数不可控,可能使用无界队列或者创建过多线程,导致任务堆积、内存溢出或者系统资源耗尽。实际开发中更推荐使用 ThreadPoolExecutor 手动指定核心线程数、最大线程数、队列容量和拒绝策略。

例如:

java 复制代码
Executors.newFixedThreadPool(10)

底层使用的是近似无界的 LinkedBlockingQueue

如果任务太多,会一直堆积在队列里,可能导致 OOM。

java 复制代码
Executors.newCachedThreadPool()

最大线程数非常大,如果任务很多,可能创建大量线程,把系统压垮。


6. 怎么合理设置线程池参数?

面试可能会问:

线程池参数怎么设置?

这题没有固定答案,要看任务类型。


CPU 密集型任务

比如:

text 复制代码
大量计算
图像处理
加密解密
复杂算法

这种任务主要消耗 CPU。

线程数一般设置为:

text 复制代码
CPU 核心数 + 1

比如 8 核 CPU:

text 复制代码
核心线程数可以设置为 8 或 9

因为线程太多反而会导致 CPU 频繁上下文切换。


IO 密集型任务

比如:

text 复制代码
查数据库
调用接口
读写文件
网络请求
FTP下载

这种任务经常在等待 IO。

线程数可以设置得比 CPU 核心数大一些,比如:

text 复制代码
CPU 核心数 * 2

或者根据压测结果调整。

例如 8 核 CPU,可以先尝试:

text 复制代码
16、24、32

然后根据 CPU 使用率、响应时间、队列积压情况调优。


7. 一个实际开发中的线程池配置示例

比如订单系统里,异步处理订单通知:

java 复制代码
import java.util.concurrent.*;

public class OrderThreadPool {

    private static final ThreadPoolExecutor ORDER_EXECUTOR =
            new ThreadPoolExecutor(
                    5,
                    10,
                    60,
                    TimeUnit.SECONDS,
                    new ArrayBlockingQueue<>(100),
                    r -> {
                        Thread thread = new Thread(r);
                        thread.setName("order-task-" + thread.getId());
                        return thread;
                    },
                    new ThreadPoolExecutor.CallerRunsPolicy()
            );

    public static void submitTask(Runnable task) {
        ORDER_EXECUTOR.execute(task);
    }
}

这个r是什么

java 复制代码
 r -> {
                        Thread thread = new Thread(r);
                        thread.setName("order-task-" + thread.getId());
                        return thread;
                    },

这里的 r 本质上是一个 Runnable 任务对象。

也就是说,线程池要创建线程时,会把一个 Runnable 传进来,然后你用这个 Runnable 创建真正的 Thread。

使用:

java 复制代码
OrderThreadPool.submitTask(() -> {
    System.out.println("异步处理订单通知");
});

8. execute 和 submit 有什么区别?

这个也经常问。

execute

java 复制代码
executor.execute(() -> {
    System.out.println("任务执行");
});

特点:

text 复制代码
只能提交 Runnable
没有返回值
异常会直接抛到线程的异常处理器

submit

java 复制代码
Future<Integer> future = executor.submit(() -> {
    return 1 + 1;
});

Integer result = future.get();

特点:

text 复制代码
可以提交 Runnable,也可以提交 Callable
可以有返回值
返回 Future
异常会被封装到 Future 里,调用 get() 时才抛出

简单记忆:

text 复制代码
execute:只管执行
submit:执行后还能拿结果

9. shutdown 和 shutdownNow 有什么区别?

shutdown

java 复制代码
executor.shutdown();

表示:

text 复制代码
不再接收新任务
已经提交的任务继续执行
队列里的任务也会继续执行

比较温和。


shutdownNow

java 复制代码
executor.shutdownNow();

表示:

text 复制代码
尝试中断正在执行的任务
队列中未执行的任务会返回

比较强硬,但不一定能立刻停掉正在执行的任务。


10. 最后给你一个面试版完整回答

如果面试官问:

讲一下线程池?

你可以这样答:

线程池是一种线程复用机制,底层核心实现是 ThreadPoolExecutor。它会维护一批线程,任务提交后由线程池分配线程执行,线程执行完任务后不会立即销毁,而是继续复用。使用线程池的好处是可以减少线程频繁创建和销毁的开销,同时可以限制并发线程数量,避免线程无限创建导致 CPU 上下文切换过多或者内存溢出,并且可以统一管理任务队列、拒绝策略、线程工厂等。

ThreadPoolExecutor 有 7 个核心参数,分别是核心线程数 corePoolSize、最大线程数 maximumPoolSize、线程空闲存活时间 keepAliveTime、时间单位 unit、任务队列 workQueue、线程工厂 threadFactory 和拒绝策略 handler

它的执行流程是:任务提交后,如果当前线程数小于核心线程数,就创建核心线程执行;如果核心线程都忙,就把任务放入阻塞队列;如果队列满了,并且当前线程数小于最大线程数,就创建非核心线程执行;如果线程数达到最大线程数并且队列也满了,就触发拒绝策略。

实际开发中不建议直接使用 Executors 创建线程池,因为它的一些默认实现可能使用无界队列或者创建过多线程,存在 OOM 或资源耗尽风险。一般推荐手动使用 ThreadPoolExecutor 明确指定参数。


一句话总结

线程池的本质就是:

用有限数量的线程,复用执行大量任务,既提升效率,又控制风险。

你重点记住这个流程就行:

text 复制代码
核心线程 -> 队列 -> 最大线程 -> 拒绝策略

这句话面试非常常用。

相关推荐
SilentSamsara1 小时前
爬虫工程化:Playwright + 反反爬 + 数据清洗管道实战
开发语言·爬虫·python·青少年编程·playwright
AI玫瑰助手1 小时前
Python函数:函数的返回值(return)与多值返回
开发语言·python·信息可视化
花果山~~程序猿1 小时前
快速认识python项目的虚拟环境
开发语言·python
gCode Teacher 格码致知1 小时前
Python教学:字符编码的四种环境-由Deepseek产生
开发语言·python
小江的记录本2 小时前
【JVM虚拟机】类加载机制:类加载器、双亲委派模型、好处、破坏双亲委派的场景(附《思维导图》+《面试高频考点清单》)
java·jvm·spring boot·后端·python·spring·面试
小陶来咯2 小时前
FunctionCall实现与Prompt调优
python·ai·prompt
AI 编程助手GPT2 小时前
ChatGPT 新手入门与实战操作指南
开发语言·人工智能·git·python·chatgpt
原创小甜甜2 小时前
OOM 排查复盘:Hutool 序列化 Request 导致 Java Heap Space
java·开发语言·python
gf13211112 小时前
【精确查找python脚本是否在运行】
linux·前端·python