下面我用一个生活类比先帮你建立感觉:线程池就像一个"外卖骑手站点"。
如果每来一个订单,就临时招聘一个骑手,送完就解雇,这很浪费。线程也是一样:如果每来一个任务,就 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
核心线程 -> 队列 -> 最大线程 -> 拒绝策略
这句话面试非常常用。