Java 面试:线程池 7 个核心参数怎么理解?
摘要
线程池是 Java 面试里非常高频的并发题。很多人能背出 corePoolSize、maximumPoolSize、workQueue,但一问线程池执行流程、拒绝策略、为什么不建议使用 Executors,就容易答乱。本文用面试视角,结合代码案例,梳理线程池的作用、7 个核心参数、任务执行流程、常见队列、拒绝策略以及实际开发中的使用建议。
前言
Java 后端面试里,线程池基本是绕不开的。
这个题看起来是八股,但实际开发中也经常用到,比如:
- 异步处理;
- 批量任务;
- 定时任务;
- 多线程导入;
- 报表计算;
- 接口并发调用;
- 消息消费加速。
很多人背线程池,喜欢直接背 7 个参数。
但面试官真正想听的,不只是参数名,而是:
线程池为什么需要这些参数?任务提交进来后到底怎么执行?队列满了怎么办?线程数什么时候增加?什么时候拒绝?
这篇就用比较直接的方式,把线程池讲清楚。
一、面试官一般怎么问?
线程池常见问法有这些:
- 线程池是干什么的?
- 为什么要用线程池?
- 线程池 7 个核心参数是什么?
- 线程池任务执行流程是什么?
corePoolSize和maximumPoolSize有什么区别?- 常见拒绝策略有哪些?
- 常见阻塞队列有哪些?
- 为什么不建议使用
Executors创建线程池? - 实际开发中线程池参数怎么配置?
- 线程池使用不当会有什么问题?
二、线程池是干什么的?
一句话:
线程池是用来复用线程、控制并发数量、减少线程频繁创建和销毁成本的。
如果每来一个任务就创建一个线程:
new Thread(() -> {
// 执行业务逻辑
}).start();
问题很明显:
- 线程频繁创建和销毁,有性能损耗;
- 线程数量不可控,容易把 CPU、内存打满;
- 没有统一管理,不方便监控和关闭;
- 任务多的时候,系统容易被拖垮。
线程池就是为了解决这些问题。
它可以把线程提前创建好,任务来了之后交给线程池执行。
三、线程池 7 个核心参数
自定义线程池一般使用 ThreadPoolExecutor。
构造方法如下:
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
) {
// ...
}
这 7 个参数分别是:
|
参数 |
含义 |
|---|---|
|
|
核心线程数 |
|
|
最大线程数 |
|
|
非核心线程空闲存活时间 |
|
|
时间单位 |
|
|
任务队列 |
|
|
线程工厂 |
|
|
拒绝策略 |
四、corePoolSize:核心线程数
corePoolSize 表示核心线程数。
简单理解:
线程池里长期保留的基础线程数量。
比如:
corePoolSize = 5
表示线程池会优先创建 5 个核心线程来处理任务。
只要任务不断进来,核心线程会一直负责执行。
默认情况下,核心线程即使空闲,也不会被销毁。
除非设置:
threadPoolExecutor.allowCoreThreadTimeOut(true);
五、maximumPoolSize:最大线程数
maximumPoolSize 表示线程池允许创建的最大线程数。
比如:
corePoolSize = 5
maximumPoolSize = 10
意思是:
- 平时先用 5 个核心线程处理任务;
- 如果任务太多,队列也满了;
- 线程池最多可以扩到 10 个线程。
需要注意:
不是任务一多,就马上创建到 maximumPoolSize。
线程池会先看核心线程,再看队列,最后才考虑创建非核心线程。
这个执行顺序很重要。
六、workQueue:任务队列
workQueue 是任务队列。
当核心线程都忙时,新任务会先进入队列等待。
常见队列有:
1. ArrayBlockingQueue
有界阻塞队列,底层是数组。
new ArrayBlockingQueue<>(100);
特点:
- 队列长度固定;
- 满了之后不能继续放;
- 更容易控制内存风险。
2. LinkedBlockingQueue
链表阻塞队列。
new LinkedBlockingQueue<>(100);
也可以不指定容量,但不建议。
如果不指定容量,默认容量非常大,容易堆积大量任务,导致内存风险。
3. SynchronousQueue
不存储任务的队列。
任务提交后必须马上有线程接收,否则就创建新线程或触发拒绝策略。
newCachedThreadPool 里面就用了它。
4. PriorityBlockingQueue
优先级队列。
可以让任务按优先级执行。
但实际开发中用得相对少一些。
七、keepAliveTime 和 unit
keepAliveTime 表示非核心线程空闲多久后会被回收。
比如:
keepAliveTime = 60
unit = TimeUnit.SECONDS
意思是:
非核心线程空闲超过 60 秒,就会被销毁。
注意,一般情况下,这个参数主要作用于非核心线程。
核心线程默认不会因为空闲而销毁。
八、threadFactory:线程工厂
threadFactory 用来创建线程。
实际开发中建议自定义线程名称。
因为线上排查问题时,线程名非常重要。
例如:
ThreadFactory threadFactory = new ThreadFactory() {
private final AtomicInteger index = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("order-task-pool-" + index.getAndIncrement());
return thread;
}
};
这样日志里能看到线程名:
order-task-pool-1
order-task-pool-2
如果线程池没有命名,线上排查会很难受。
九、handler:拒绝策略
当线程池已经达到最大线程数,并且任务队列也满了,新任务就会触发拒绝策略。
JDK 提供了 4 种常见拒绝策略。
1. AbortPolicy
默认策略,直接抛异常。
new ThreadPoolExecutor.AbortPolicy()
会抛:
RejectedExecutionException
2. CallerRunsPolicy
由提交任务的线程自己执行任务。
new ThreadPoolExecutor.CallerRunsPolicy()
这个策略不会直接丢任务。
但会让提交任务的线程变慢,相当于一种反压。
3. DiscardPolicy
直接丢弃任务,不抛异常。
new ThreadPoolExecutor.DiscardPolicy()
这个要慎用。
因为任务丢了,你可能还不知道。
4. DiscardOldestPolicy
丢弃队列中最老的任务,然后尝试提交当前任务。
new ThreadPoolExecutor.DiscardOldestPolicy()
这个也要慎用。
因为被丢弃的老任务可能也很重要。
十、线程池任务执行流程
这个是面试重点。
任务提交到线程池后,大概按这个顺序执行:
1. 如果当前线程数小于 corePoolSize,创建核心线程执行任务
2. 如果核心线程数已满,任务进入队列
3. 如果队列满了,并且当前线程数小于 maximumPoolSize,创建非核心线程执行任务
4. 如果线程数已经达到 maximumPoolSize,并且队列也满了,触发拒绝策略
一句话版本:
先核心线程,再任务队列,再最大线程,最后拒绝策略。
这个流程一定要记住。
十一、完整代码案例:自定义线程池
下面写一个完整示例。
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class CustomThreadPoolDemo {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2,
4,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3),
new NamedThreadFactory("biz-task-pool"),
new ThreadPoolExecutor.CallerRunsPolicy()
);
for (int i = 1; i <= 10; i++) {
int taskNo = i;
executor.execute(() -> {
System.out.println(Thread.currentThread().getName() + " 执行任务:" + taskNo);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
executor.shutdown();
}
static class NamedThreadFactory implements ThreadFactory {
private final String prefix;
private final AtomicInteger index = new AtomicInteger(1);
public NamedThreadFactory(String prefix) {
this.prefix = prefix;
}
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName(prefix + "-" + index.getAndIncrement());
return thread;
}
}
}
这个线程池配置是:
核心线程数:2
最大线程数:4
队列容量:3
拒绝策略:CallerRunsPolicy
任务执行过程大概是:
前 2 个任务创建核心线程执行
接下来 3 个任务进入队列
队列满了之后,再创建非核心线程
线程达到最大值后,再来的任务触发拒绝策略
十二、观察线程池执行流程
为了更直观看线程池状态,可以打印线程池信息。
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadPoolStatusDemo {
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2,
4,
30,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(2),
new NamedThreadFactory("status-pool"),
new ThreadPoolExecutor.AbortPolicy()
);
for (int i = 1; i <= 6; i++) {
int taskNo = i;
executor.execute(() -> {
printStatus(executor, "执行任务前:" + taskNo);
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
printStatus(executor, "执行任务后:" + taskNo);
});
}
TimeUnit.SECONDS.sleep(10);
executor.shutdown();
}
private static void printStatus(ThreadPoolExecutor executor, String message) {
System.out.println("[" + message + "]"
+ " poolSize=" + executor.getPoolSize()
+ ", activeCount=" + executor.getActiveCount()
+ ", queueSize=" + executor.getQueue().size()
+ ", completedTaskCount=" + executor.getCompletedTaskCount());
}
static class NamedThreadFactory implements ThreadFactory {
private final String prefix;
private final AtomicInteger index = new AtomicInteger(1);
public NamedThreadFactory(String prefix) {
this.prefix = prefix;
}
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName(prefix + "-" + index.getAndIncrement());
return thread;
}
}
}
这里可以看到几个指标:
|
方法 |
含义 |
|---|---|
|
|
当前线程池线程数 |
|
|
正在执行任务的线程数 |
|
|
当前队列中等待的任务数 |
|
|
已完成任务数 |
这些指标在排查线程池问题时很有用。
十三、拒绝策略代码案例
下面演示默认拒绝策略 AbortPolicy。
import java.util.concurrent.*;
public class RejectPolicyDemo {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
1,
1,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
for (int i = 1; i <= 3; i++) {
int taskNo = i;
executor.execute(() -> {
System.out.println(Thread.currentThread().getName() + " 执行任务:" + taskNo);
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
executor.shutdown();
}
}
这个线程池配置是:
核心线程数:1
最大线程数:1
队列容量:1
提交 3 个任务时:
- 第 1 个任务由线程执行;
- 第 2 个任务进入队列;
- 第 3 个任务无法执行,也无法入队;
- 触发拒绝策略。
最终会抛出:
RejectedExecutionException
十四、为什么不建议使用 Executors?
JDK 提供了 Executors 工具类,可以快速创建线程池。
比如:
ExecutorService executor = Executors.newFixedThreadPool(10);
看起来很方便。
但实际开发中不建议直接使用。
原因是很多默认配置不够可控。
1. newFixedThreadPool
Executors.newFixedThreadPool(10);
底层使用的是无界队列:
LinkedBlockingQueue
如果任务提交速度远大于消费速度,队列会不断堆积,可能导致 OOM。
2. newSingleThreadExecutor
Executors.newSingleThreadExecutor();
它也是使用无界队列。
如果任务太多,同样可能堆积大量任务。
3. newCachedThreadPool
Executors.newCachedThreadPool();
它的最大线程数非常大。
如果任务非常多,可能创建大量线程,导致系统资源被打爆。
所以更推荐手动创建 ThreadPoolExecutor。
这样核心线程数、最大线程数、队列长度、线程名称、拒绝策略都可以自己控制。
十五、Executors 问题演示
下面是一个不推荐的例子。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ExecutorsRiskDemo {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
for (int i = 1; i <= 100000; i++) {
int taskNo = i;
executor.execute(() -> {
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("执行任务:" + taskNo);
});
}
executor.shutdown();
}
}
这个例子的问题是:
任务提交非常快
线程只有 2 个
任务执行很慢
队列会不断堆积
如果任务量非常大,就可能导致内存压力。
这就是为什么不建议无脑使用 Executors.newFixedThreadPool()。
十六、实际开发中线程池怎么配置?
线程池参数没有万能公式。
一般要根据任务类型来配置。
1. CPU 密集型任务
比如:
- 复杂计算;
- 加密解密;
- 大量对象转换;
- 本地计算。
这类任务主要消耗 CPU。
线程数不宜太多。
一般可以参考:
CPU 核心数 + 1
示例:
int cpuCount = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor executor = new ThreadPoolExecutor(
cpuCount + 1,
cpuCount + 1,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(500),
new NamedThreadFactory("cpu-task-pool"),
new ThreadPoolExecutor.CallerRunsPolicy()
);
2. IO 密集型任务
比如:
- 查数据库;
- 调远程接口;
- 读写文件;
- 网络请求。
这类任务经常在等待 IO 返回。
线程数可以比 CPU 核心数多一些。
比如:
CPU 核心数 * 2
或者根据接口耗时、QPS、机器资源压测后调整。
示例:
int cpuCount = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor executor = new ThreadPoolExecutor(
cpuCount * 2,
cpuCount * 4,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1000),
new NamedThreadFactory("io-task-pool"),
new ThreadPoolExecutor.CallerRunsPolicy()
);
注意:
这只是参考,不是绝对标准。
真实项目里还是要看任务耗时、数据量、数据库连接池、下游接口承载能力。
十七、实际案例:批量任务使用线程池
比如有一批用户数据,需要调用远程接口处理。
不要直接在循环里串行处理:
for (Long userId : userIds) {
processUser(userId);
}
可以用线程池并发处理。
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class BatchTaskDemo {
public static void main(String[] args) throws InterruptedException {
List<Long> userIds = Arrays.asList(1001L, 1002L, 1003L, 1004L, 1005L);
ThreadPoolExecutor executor = new ThreadPoolExecutor(
3,
6,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
new NamedThreadFactory("batch-user-pool"),
new ThreadPoolExecutor.CallerRunsPolicy()
);
CountDownLatch latch = new CountDownLatch(userIds.size());
for (Long userId : userIds) {
executor.execute(() -> {
try {
processUser(userId);
} finally {
latch.countDown();
}
});
}
latch.await();
executor.shutdown();
System.out.println("批量任务处理完成");
}
private static void processUser(Long userId) {
System.out.println(Thread.currentThread().getName() + " 处理用户:" + userId);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
static class NamedThreadFactory implements ThreadFactory {
private final String prefix;
private final AtomicInteger index = new AtomicInteger(1);
public NamedThreadFactory(String prefix) {
this.prefix = prefix;
}
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName(prefix + "-" + index.getAndIncrement());
return thread;
}
}
}
这里用了:
- 自定义线程池;
- 自定义线程名称;
- 有界队列;
CallerRunsPolicy;CountDownLatch等待所有任务完成。
这种写法在批量任务里比较常见。
十八、线程池使用注意事项
1. 不要无脑创建很多线程池
线程池不是越多越好。
一个系统里如果到处 new 线程池,很容易导致线程数不可控。
建议按业务类型统一管理。
2. 队列不要无界
无界队列容易堆积任务。
任务量一大,可能导致内存风险。
建议使用有界队列。
3. 线程要命名
线程名称很重要。
线上排查问题时,通过线程名可以快速定位是哪个业务线程池。
4. 拒绝策略要根据业务选择
如果任务不能丢,别用 DiscardPolicy。
如果允许调用线程自己执行,可以考虑 CallerRunsPolicy。
如果任务非常重要,要记录日志或落库补偿。
5. 记得关闭线程池
如果线程池不再使用,需要关闭。
executor.shutdown();
如果需要强制关闭:
executor.shutdownNow();
一般优先使用 shutdown()。
十九、面试回答模板
如果面试官问:
线程池 7 个参数是什么?执行流程是什么?
可以这样回答:
线程池主要用来复用线程、控制并发数量,避免频繁创建线程带来的性能损耗,也避免线程数量失控。
ThreadPoolExecutor 有 7 个核心参数:
corePoolSize 是核心线程数;
maximumPoolSize 是最大线程数;
keepAliveTime 和 unit 表示非核心线程空闲多久后回收;
workQueue 是任务队列;
threadFactory 用来创建线程,通常会自定义线程名;
handler 是拒绝策略。
线程池执行任务时,先看核心线程数是否已满。
如果核心线程没满,就创建核心线程执行任务;
如果核心线程满了,就把任务放入队列;
如果队列也满了,并且线程数还没到 maximumPoolSize,就创建非核心线程;
如果线程数已经达到 maximumPoolSize,队列也满了,就触发拒绝策略。
实际开发中不建议直接使用 Executors,因为很多默认队列是无界的,可能导致任务堆积和 OOM。更推荐手动创建 ThreadPoolExecutor,明确线程数、队列长度、线程名称和拒绝策略。
这段回答基本够用了。
二十、一句话总结
线程池不是只背 7 个参数。
更重要的是理解:
任务提交后,线程池到底怎么处理?
核心流程是:
核心线程
↓
任务队列
↓
非核心线程
↓
拒绝策略
面试时只要把下面几点讲清楚:
- 线程池为什么存在;
- 7 个参数分别干什么;
- 任务执行流程;
- 常见队列和拒绝策略;
- 为什么不建议使用 Executors;
- 实际开发怎么配置;
基本就不会乱。
大家加油:)
更多推荐

所有评论(0)