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()会抛:
RejectedExecutionException2. 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;
- 实际开发怎么配置;
基本就不会乱。
大家加油:)