一、基础概念
Q1:什么是线程池?为什么需要线程池?
答: 线程池是线程的复用容器,预先创建或按需创建线程,统一管理生命周期。
使用线程池的好处:
// 不用线程池:每次新建,开销大
new Thread(() -> doWork()).start();
// 用线程池:复用线程
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.execute(() -> doWork());Q2:ThreadPoolExecutor 的七大参数是什么?
答:
public ThreadPoolExecutor(
int corePoolSize, // 1. 核心线程数
int maximumPoolSize, // 2. 最大线程数
long keepAliveTime, // 3. 空闲存活时间
TimeUnit unit, // 4. 时间单位
BlockingQueue<Runnable> workQueue, // 5. 任务队列
ThreadFactory threadFactory, // 6. 线程工厂
RejectedExecutionHandler handler // 7. 拒绝策略
)Q3:corePoolSize 和 maximumPoolSize 有什么区别?
答:
corePoolSize:核心线程数,线程池长期保持的线程数
maximumPoolSize:最大线程数,临时扩容的上限
关系:
corePoolSize ≤ 当前线程数 ≤ maximumPoolSize关键点:
核心线程默认不回收(除非
allowCoreThreadTimeOut(true))非核心线程空闲超过
keepAliveTime会被回收只有当队列满时,才会创建非核心线程
Q4:keepAliveTime 的作用是什么?
答: 控制非核心线程的空闲存活时间:
// 非核心线程空闲 60 秒后回收
new ThreadPoolExecutor(2, 10, 60L, TimeUnit.SECONDS, ...);注意:
默认只作用于非核心线程
调用
allowCoreThreadTimeOut(true)后,核心线程也会被回收回收的是空闲线程,忙碌线程不受影响
Q5:workQueue 有哪些类型?如何选择?
答: 常见队列:
// 有界队列,推荐
new ArrayBlockingQueue<>(100);
// 无界队列,有 OOM 风险
new LinkedBlockingQueue<>();选择原则:必须用有界队列,避免任务堆积导致 OOM。
Q6:threadFactory 的作用是什么?
答: 自定义线程创建方式,常用于:
设置有意义的线程名(排查问题)
设置守护线程
设置优先级、异常处理器
ThreadFactory factory = new ThreadFactory() {
private final AtomicInteger count = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, "my-pool-" + count.getAndIncrement());
t.setDaemon(false);
t.setUncaughtExceptionHandler((thread, e) ->
log.error("线程异常", e));
return t;
}
};推荐:用 Guava 的 ThreadFactoryBuilder 更简洁。
二、工作流程
Q7:线程池的完整工作流程是什么?
答: 提交任务时,按以下顺序处理:
提交任务
↓
1. 核心线程未满? → 创建核心线程执行
↓ 否
2. 队列未满? → 放入队列等待
↓ 否
3. 线程数 < max? → 创建非核心线程执行
↓ 否
4. 执行拒绝策略流程图:
execute(task)
│
├─ 线程数 < corePoolSize → addWorker(core)
│
├─ 队列未满 → workQueue.offer(task)
│
├─ 线程数 < maximumPoolSize → addWorker(non-core)
│
└─ 拒绝策略关键:先队列,后扩容,这是最容易记错的点。
Q8:为什么线程池先入队列,而不是先创建线程?
答: 这是设计权衡:
创建线程开销大,能复用就复用
队列能缓冲任务,避免频繁创建销毁
只有队列满(说明任务积压)才扩容
但这也带来问题:
如果队列无界,
maximumPoolSize永远用不到任务会一直堆积,可能导致 OOM
最佳实践:用有界队列,让扩容和拒绝策略生效。
Q9:核心线程是何时创建的?
答: 默认懒创建——提交任务时才创建。
预创建核心线程:
ThreadPoolExecutor pool = new ThreadPoolExecutor(...);
pool.prestartAllCoreThreads(); // 启动所有核心线程
pool.prestartCoreThread(); // 启动一个核心线程好处:提前创建,避免首次任务延迟。
Q10:线程池中的线程是如何复用的?
答: 通过 runWorker 循环 + getTask 阻塞:
final void runWorker(Worker w) {
Runnable task = w.firstTask;
w.firstTask = null;
while (task != null || (task = getTask()) != null) {
task.run(); // 执行任务
task = null;
}
}流程:
Worker 线程启动后进入
runWorker循环从队列
getTask()取任务队列空则阻塞(
take()或poll(keepAliveTime))取到任务执行,执行完继续循环
线程不销毁,实现复用
Q11:线程池中的线程何时销毁?
答:
非核心线程:空闲超过
keepAliveTime后,getTask()返回 null,退出循环销毁核心线程:默认不销毁,除非
allowCoreThreadTimeOut(true)线程池关闭:
shutdown()/shutdownNow()后销毁
// getTask 中的回收逻辑
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null; // 返回 null,线程退出
}Q12:execute() 和 submit() 有什么区别?
答:
// execute:异常直接抛出
pool.execute(() -> { throw new RuntimeException(); });
// submit:异常在 get() 时抛出
Future<?> f = pool.submit(() -> { throw new RuntimeException(); });
f.get(); // 抛 ExecutionException注意:submit() 的异常不 get() 就被吞掉,容易踩坑。
Q13:线程池的 ctl 字段是什么?
答: ctl 是 AtomicInteger,打包了线程数和状态:
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// 高 3 位:线程池状态
// 低 29 位:工作线程数
private static final int COUNT_BITS = Integer.SIZE - 3; // 29
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// 状态
RUNNING = -1 << 29 // 接受新任务,处理队列任务
SHUTDOWN = 0 << 29 // 不接受新任务,处理队列任务
STOP = 1 << 29 // 不接受新任务,不处理队列,中断线程
TIDYING = 2 << 29 // 所有任务终止,即将调用 terminated()
TERMINATED = 3 << 29 // terminated() 完成优点:一个原子变量同时管理状态和数量,避免锁。
Q14:线程池的五种状态如何流转?
答:
RUNNING ──shutdown()──→ SHUTDOWN ──队列空+线程空──→ TIDYING ──→ TERMINATED
│
└──shutdownNow()──→ STOP ──线程空──→ TIDYING ──→ TERMINATEDQ15:shutdown() 和 shutdownNow() 有什么区别?
答:
pool.shutdown(); // 优雅关闭
pool.shutdownNow(); // 强制关闭,返回未执行任务最佳实践:
pool.shutdown();
if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
pool.shutdownNow();
}Q16:isShutdown() 和 isTerminated() 有什么区别?
答:
isShutdown():是否已调用shutdown()或shutdownNow(),状态 ≥ SHUTDOWNisTerminated():是否完全终止,状态 == TERMINATED
pool.shutdown();
System.out.println(pool.isShutdown()); // true
System.out.println(pool.isTerminated()); // false,可能还有任务
// 等待所有任务完成
pool.awaitTermination(1, TimeUnit.MINUTES);
System.out.println(pool.isTerminated()); // true三、拒绝策略
Q17:什么是拒绝策略?何时触发?
答: 当线程数达到 maximumPoolSize 且队列已满时,新任务被拒绝。
触发条件:
线程数 == maximumPoolSize
队列已满
提交新任务
// 触发拒绝的场景
ThreadPoolExecutor pool = new ThreadPoolExecutor(
1, 1, 0, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1)); // 队列容量 1
pool.execute(task1); // 核心线程执行
pool.execute(task2); // 入队列
pool.execute(task3); // 触发拒绝策略Q18:JDK 内置的四种拒绝策略是什么?
答:
// 1. AbortPolicy(默认)
new ThreadPoolExecutor.AbortPolicy();
// 2. CallerRunsPolicy
new ThreadPoolExecutor.CallerRunsPolicy();
// 3. DiscardPolicy
new ThreadPoolExecutor.DiscardPolicy();
// 4. DiscardOldestPolicy
new ThreadPoolExecutor.DiscardOldestPolicy();Q19:CallerRunsPolicy 的原理和适用场景?
答: 拒绝时,由提交任务的线程自己执行:
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run(); // 调用者线程执行
}
}效果:
提交线程被阻塞,无法继续提交
形成负反馈,自然降速
保证任务不丢失
适用:
不允许丢任务
能接受提交线程变慢
如日志、非核心业务
风险:若提交线程是关键线程(如 IO 线程),可能拖垮整体。
Q20:如何自定义拒绝策略?
答: 实现 RejectedExecutionHandler:
public class CustomRejectPolicy implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
// 1. 记录日志
log.warn("任务被拒绝: {}", r);
// 2. 持久化到数据库/MQ,后续补偿
saveToDb(r);
// 3. 或降级处理
// fallback(r);
}
}常见自定义策略:
持久化后重试
降级到本地执行
报警 + 丢弃
阻塞等待(类似 CallerRuns)
Q21:为什么推荐 CallerRunsPolicy 或自定义策略?
答:
AbortPolicy:抛异常,若未捕获则任务丢失
DiscardPolicy:静默丢弃,问题难发现
DiscardOldestPolicy:丢老任务,可能丢关键任务
CallerRunsPolicy:不丢任务,但阻塞提交者
推荐:
核心业务:自定义(持久化 + 重试)
非核心:CallerRunsPolicy
绝不推荐:DiscardPolicy(无感知丢失)
Q22:拒绝策略能保证任务不丢失吗?
答: 取决于策略:
结论:只有 CallerRuns 和自定义持久化能保证不丢。
四、线程池大小设置
Q23:如何设置线程池大小?
答: 根据任务类型决定:
int cpuCores = Runtime.getRuntime().availableProcessors();
// CPU 密集型
int cpuPoolSize = cpuCores + 1;
// IO 密集型
int ioPoolSize = cpuCores * 2;原因:
CPU 密集型:线程多了上下文切换开销大
IO 密集型:线程等待 IO 时 CPU 空闲,可多开
Q24:为什么 CPU 密集型是 N+1?
答:
N 个线程跑满 CPU
+1 是为了容错:某线程偶尔缺页/暂停时,第 N+1 个能顶上
太多线程会导致上下文切换开销
验证:CPU 利用率接近 100%,但线程数不宜过多。
Q25:为什么 IO 密集型是 2N?
答:
线程等待 IO 时不占 CPU
可以多开线程,让 CPU 在等待时切换执行其他任务
2N 是经验值,实际要看IO 等待时间 / CPU 计算时间的比值
更精确的公式:
线程数 = N × (1 + 等待时间 / 计算时间)// 例:CPU 4 核,等待 100ms,计算 50ms
// 线程数 = 4 × (1 + 100/50) = 12Q26:线程池大小设置过大或过小有什么问题?
答:
结论:不是越大越好,要压测调优。
Q27:如何动态调整线程池大小?
答: ThreadPoolExecutor 提供 setter:
ThreadPoolExecutor pool = new ThreadPoolExecutor(...);
pool.setCorePoolSize(20);
pool.setMaximumPoolSize(50);
pool.setKeepAliveTime(30, TimeUnit.SECONDS);注意:
调大 core:立即生效,可能创建新线程
调小 core:空闲线程会被回收
调小 max:若当前线程数 > 新 max,多余线程会被回收
结合配置中心可实现动态调整(如 Nacos + 定时任务)。
Q28:如何监控线程池状态?
答: 通过以下方法:
pool.getPoolSize(); // 当前线程数
pool.getCorePoolSize(); // 核心线程数
pool.getMaximumPoolSize(); // 最大线程数
pool.getActiveCount(); // 活跃线程数
pool.getQueue().size(); // 队列任务数
pool.getCompletedTaskCount();// 已完成任务数
pool.getTaskCount(); // 总任务数
pool.getLargestPoolSize(); // 历史最大线程数监控指标:
活跃度 = activeCount / maximumPoolSize
队列使用率 = queue.size() / queue.capacity()
拒绝次数(自定义策略统计)
Q29:为什么阿里规范禁止用 Executors 创建线程池?
答: 因为 Executors 的默认实现有 OOM 风险:
// 危险:无界队列
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()); // 无界!
}推荐:手动 new ThreadPoolExecutor,指定有界队列和拒绝策略。
Q30:如何合理配置一个线程池?
答: 完整示例:
ThreadPoolExecutor pool = new ThreadPoolExecutor(
8, // core:CPU 核数
16, // max:2 倍
60L, TimeUnit.SECONDS, // 空闲回收
new ArrayBlockingQueue<>(200), // 有界队列
new ThreadFactoryBuilder()
.setNameFormat("biz-pool-%d")
.build(),
new ThreadPoolExecutor.CallerRunsPolicy() // 不丢任务
);
// 允许核心线程超时回收
pool.allowCoreThreadTimeOut(true);
// 预启动核心线程
pool.prestartAllCoreThreads();配置原则:
有界队列
有意义的线程名
明确的拒绝策略
根据任务类型定大小
监控 + 动态调整
五、源码与进阶
Q31:addWorker 的作用是什么?
答: 创建并启动工作线程:
private boolean addWorker(Runnable firstTask, boolean core) {
// 1. CAS 增加 workerCount
// 2. new Worker(firstTask)
// 3. 启动线程 t.start()
// 4. 加入 workers 集合
}要点:
core=true:检查是否 < corePoolSizecore=false:检查是否 < maximumPoolSize失败返回 false,触发拒绝策略
Q32:getTask() 如何实现阻塞和超时?
答:
private Runnable getTask() {
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
for (;;) {
if (timed) {
// 非核心线程:超时获取
Runnable r = workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS);
if (r != null) return r;
timedOut = true;
} else {
// 核心线程:阻塞获取
Runnable r = workQueue.take();
if (r != null) return r;
}
// 超时或状态异常,返回 null,线程退出
}
}区别:
核心线程:
take()永久阻塞非核心线程:
poll(keepAliveTime)超时返回 null
Q33:Worker 为什么要继承 AQS?
答: Worker 继承 AQS 是为了控制中断状态:
private final class Worker extends AbstractQueuedSynchronizer
implements Runnable {
final Thread thread;
Runnable firstTask;
volatile long completedTasks;
}原因:
线程池在
shutdown时要区分:任务执行中 vs 空闲用 AQS 的 state 表示是否持有锁
持有锁:正在执行任务,不应中断
未持有锁:空闲,可中断
避免:中断正在执行的任务导致数据不一致。
Q34:线程池如何处理异常?
答:
execute 的异常:
pool.execute(() -> {
throw new RuntimeException("error");
});
// 线程打印异常后死亡,线程池补充新线程submit 的异常:
Future<?> f = pool.submit(() -> {
throw new RuntimeException("error");
});
try {
f.get(); // 抛 ExecutionException
} catch (ExecutionException e) {
Throwable cause = e.getCause(); // 原始异常
}最佳实践:任务内 try-catch,避免线程死亡。
Q35:线程池中的线程死了会怎样?
答: 线程池会补充新线程:
线程因异常退出,
processWorkerExit被调用workerCount减 1如果线程池还在运行,会
addWorker(null, false)补充
// processWorkerExit 简化
if (runState < STOP) {
addWorker(null, false); // 补充线程
}注意:频繁异常退出会导致线程反复创建,性能下降。
Q36:如何优雅关闭线程池?
答:
public void shutdownGracefully(ExecutorService pool) {
pool.shutdown(); // 1. 拒绝新任务,处理队列
try {
if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
pool.shutdownNow(); // 2. 超时强制关闭
if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
System.err.println("线程池未终止");
}
}
} catch (InterruptedException e) {
pool.shutdownNow(); // 3. 当前线程中断,也强制关闭
Thread.currentThread().interrupt(); // 恢复中断
}
}要点:
先
shutdown()优雅关闭超时后
shutdownNow()强制关闭处理
InterruptedException恢复中断标志
Q37:线程池的队列满了但线程没满,会发生什么?
答: 这是常见误解,实际流程是:
核心线程满
先入队列
队列满
才创建非核心线程
线程满 + 队列满
拒绝策略
关键:队列优先于扩容。
例外:SynchronousQueue 不存储任务,直接创建线程。
Q38:SynchronousQueue 的线程池行为有何不同?
答: SynchronousQueue 容量为 0,不存储任务:
new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<>()); // CachedThreadPool流程:
提交任务
尝试交给空闲线程
无空闲线程 → 创建新线程
线程数达 max → 拒绝
特点:
任务不排队,直接交接
适合短任务、高吞吐
风险:线程数可能爆炸
Q39:线程池如何实现任务窃取?
答: ForkJoinPool 用工作窃取:
每个线程有自己的双端队列
线程从自己队列头部取任务
空闲时从其他线程队列尾部偷任务
线程A: [任务1, 任务2, 任务3] ← 从头部取
线程B: [任务4, 任务5] ← 空闲,从A尾部偷任务3优点:减少竞争,提高 CPU 利用率。
适用:分治任务(如并行流、归并排序)。
Q40:ThreadPoolExecutor 和 ForkJoinPool 有什么区别?
答:
六、实战与踩坑
Q41:线程池参数如何根据业务调整?
答: 分场景:
场景1:Web 请求处理(IO 密集)
core = 20, max = 50, queue = 500
// 请求多,IO 等待长场景2:CPU 计算(CPU 密集)
core = N+1, max = N+1, queue = 100
// 计算为主,线程不宜多场景3:批量任务
core = 10, max = 10, queue = 1000
// 稳定吞吐,队列缓冲原则:压测 + 监控 + 动态调整。
Q42:线程池任务执行超时怎么办?
答: 三种方案:
Future.get(timeout):
Future<?> f = pool.submit(task);
try {
f.get(3, TimeUnit.SECONDS);
} catch (TimeoutException e) {
f.cancel(true); // 中断任务
}任务内自检:
pool.execute(() -> {
long start = System.currentTimeMillis();
while (!Thread.interrupted()) {
if (System.currentTimeMillis() - start > 3000) break;
// 工作...
}
});独立超时线程:定时扫描 + 取消
Q43:线程池导致 OOM 的原因有哪些?
答:
无界队列:任务堆积,队列 OOM
线程数过多:线程栈占内存
任务持有大对象:无法释放
ThreadLocal 未清理:内存泄漏
// 危险:无界队列
new LinkedBlockingQueue<>();
// 安全:有界队列
new ArrayBlockingQueue<>(1000);排查:jmap + jstack + Arthas。
Q44:如何排查线程池问题?
答:
jstack:查看线程栈
jstack <pid> | grep "pool-"Arthas:
thread --state WAITING
thread -n 3 # 最忙的3个线程自定义监控:
scheduledExecutor.scheduleAtFixedRate(() -> {
log.info("pool: active={}, queue={}, completed={}",
pool.getActiveCount(),
pool.getQueue().size(),
pool.getCompletedTaskCount());
}, 0, 10, TimeUnit.SECONDS);拒绝策略统计:自定义 handler 计数
Q45:线程池的线程名为什么重要?
答:
排查问题:jstack 中一眼识别
监控告警:按业务分组
日志追踪:MDC 传递上下文
// 不好的命名
Thread-0, Thread-1, pool-1-thread-1
// 好的命名
order-pool-1, payment-pool-2, sms-pool-1推荐:业务名-pool-序号。
评论