线程池核心参数

候选人小余在面试快手 P6 时,面试官问道:

"线程池有哪些核心参数?它们是怎么配合工作的?"

小余说:"有核心线程数、最大线程数、队列..."面试官追问:"队列有哪几种?它们的适用场景是什么?"

小余答不上来。面试官继续:"keepAliveTime 是针对什么的?allowCoreThreadTimeOut 是什么意思?"

小余彻底卡住了...

一、核心问题:线程池核心参数 🔴

1.1 问题拆解

第一层:七大参数(有哪些?)
  "ThreadPoolExecutor 的七大参数是什么?"
  考察点:corePoolSize、maximumPoolSize、workQueue、threadFactory、handler、keepAliveTime、allowCoreThreadTimeOut

第二层:协作机制(怎么配合?)
  "线程池的执行流程是什么?参数之间怎么配合?"
  考察点:execute() 的核心判断逻辑

第三层:队列选择(用什么?)
  "BlockingQueue 有哪几种?它们的特点是什么?"
  考察点:无界队列、有界队列、同步队列

1.2 ❌ 错误示范

候选人原话 A:"corePoolSize 是最小线程数,线程池不会少于这个数。"

问题诊断:如果没有开启 allowCoreThreadTimeOut,核心线程确实不会被回收。但如果队列满了且 maximumPoolSize > corePoolSize,新线程会被创建,直到达到 maximumPoolSize。

候选人原话 B:"LinkedBlockingQueue 是无界队列,可以存无限多的任务。"

问题诊断LinkedBlockingQueue 的默认容量是 Integer.MAX_VALUE,实际上是"准无界"。有界队列(如 ArrayBlockingQueue)可以防止 OOM,但需要配置拒绝策略。

1.3 标准回答

P5 级别:七大参数

ThreadPoolExecutor 的七大参数

public ThreadPoolExecutor(
    int corePoolSize,              // 1. 核心线程数
    int maximumPoolSize,           // 2. 最大线程数
    long keepAliveTime,            // 3. 空闲线程存活时间
    TimeUnit unit,                 // 4. keepAliveTime 的时间单位
    BlockingQueue<Runnable> workQueue,    // 5. 任务队列
    ThreadFactory threadFactory,   // 6. 线程工厂
    RejectedExecutionHandler handler       // 7. 拒绝策略
)

参数详解

参数含义作用
corePoolSize核心线程数线程池中始终保持存活的线程数(除非开启 allowCoreThreadTimeOut)
maximumPoolSize最大线程数线程池中允许的最大线程数
keepAliveTime空闲时间超过核心线程数的空闲线程的最大存活时间
unit时间单位keepAliveTime 的时间单位
workQueue任务队列存储待执行任务的队列
threadFactory线程工厂创建线程的工厂(可自定义线程名称、守护状态等)
handler拒绝策略队列满且达到最大线程数时的处理策略

P6 级别:参数协作机制

线程池执行流程(execute)

public void execute(Runnable command) {
    int c = ctl.get();
    // 1. 如果当前线程数 < corePoolSize,创建核心线程执行任务
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))  // true = core thread
            return;
        c = ctl.get();
    }

    // 2. 如果核心线程已满,尝试加入队列
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (!isRunning(recheck) && remove(command))
            reject(command);  // 非运行状态,移除任务
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);  // 确保至少有一个 worker
    }

    // 3. 如果队列满,尝试创建非核心线程
    else if (!addWorker(command, false))  // false = non-core thread
        reject(command);  // 拒绝任务
}

执行流程图

graph TD
    A[提交任务] --> B{线程数 < corePoolSize?}
    B -->|是| C[创建核心线程<br/>执行任务]
    B -->|否| D{队列不满?}
    D -->|是| E[加入任务队列]
    D -->|否| F{线程数 < maxPoolSize?}
    F -->|是| G[创建非核心线程<br/>执行任务]
    F -->|否| H[执行拒绝策略]

keepAliveTime 的作用

// 空闲线程的回收
if (wc > corePoolSize || timed) {
    long nl = timeout;
    if (wc == corePoolSize) nl = keepAliveTime;
    if (worker.tryLock()) {  // 尝试获取锁
        if (isObsolete(wc, nl)) {  // 超过 keepAliveTime 无任务
            worker.completedAbruptly = true;
            workers.remove(worker);
        }
    }
}

keepAliveTime 控制的是超过 corePoolSize 的那部分空闲线程的最大存活时间。如果 allowCoreThreadTimeOut = true,核心线程也会在空闲超过 keepAliveTime 后被回收。

P7 级别:队列选择与拒绝策略

任务队列的三种类型

队列类型队列特点风险
无界队列LinkedBlockingQueue()队列长度无限制内存 OOM
有界队列ArrayBlockingQueue(size)队列长度固定拒绝策略触发
同步队列SynchronousQueue()无队列,任务直接提交给线程拒绝策略频繁触发

队列选择的经验法则

  • 任务量大、耗时长:用有界队列 + 饱和策略
  • 任务量可控:用 ArrayBlockingQueue 并设置合理容量
  • 任务必须被立即处理:用 SynchronousQueue(如线程创建代价高)

拒绝策略

策略行为适用场景
AbortPolicy(默认)抛 RejectedExecutionException需要感知拒绝
CallerRunsPolicy由调用方线程执行限流(压力回退)
DiscardPolicy丢弃任务,不抛异常允许丢弃
DiscardOldestPolicy丢弃队列中最老的任务丢弃低优先级

【面试官心理】 这道题我能问到 P7 级别,是因为线程池参数的选择涉及了性能、资源管理、错误处理的综合权衡。能正确配置队列和拒绝策略的候选人说明他有生产经验的思考。

1.4 追问升级

追问 1:为什么阿里巴巴 Java 规范禁止使用 Executors 创建线程池?

Executors.newFixedThreadPool(n)Executors.newCachedThreadPool() 的问题:

  • FixedThreadPool:使用 LinkedBlockingQueue(Integer.MAX_VALUE),队列无界,高并发下任务堆积导致 OOM
  • CachedThreadPoolmaximumPoolSize = Integer.MAX_VALUE,高并发下可能创建大量线程导致 OOM

正确做法:使用 new ThreadPoolExecutor(...) 显式配置参数。

追问 2:线程工厂可以做什么?

ThreadFactory factory = new ThreadFactory() {
    private AtomicInteger counter = new AtomicInteger(1);

    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r);
        t.setName("MyPool-Worker-" + counter.getAndIncrement());
        t.setDaemon(false);  // 用户线程,非守护线程
        return t;
    }
};

线程工厂可以:设置线程名称(方便排查)、设置守护状态、设置优先级、设置未捕获异常处理器。

二、常用线程池配置 🟡

2.1 CPU 密集型

int corePoolSize = Runtime.getRuntime().availableProcessors() + 1;
int maxPoolSize = corePoolSize;
ThreadPoolExecutor pool = new ThreadPoolExecutor(
    corePoolSize, maxPoolSize,
    0L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue<>(1000),
    new ThreadFactoryBuilder().setNameFormat("CPU-Pool-%d").build(),
    new ThreadPoolExecutor.CallerRunsPolicy()
);

2.2 I/O 密集型

int corePoolSize = Runtime.getRuntime().availableProcessors() * 2;  // 或更多
int maxPoolSize = corePoolSize * 2;
ThreadPoolExecutor pool = new ThreadPoolExecutor(
    corePoolSize, maxPoolSize,
    60L, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(1000),
    new ThreadFactoryBuilder().setNameFormat("IO-Pool-%d").build(),
    new ThreadPoolExecutor.CallerRunsPolicy()
);

三、生产避坑

3.1 任务堆积导致 OOM

场景:使用 Executors.newFixedThreadPool(10) 处理请求,数据库阻塞后任务堆积,OOM。

根因LinkedBlockingQueue 默认容量 Integer.MAX_VALUE,任务堆积导致内存耗尽。

解决:使用有界队列 + 合理的拒绝策略。

3.2 线程池拒绝策略的顺序问题

// 错误:CallerRunsPolicy 在核心线程满、队列满、maxPoolSize 满时
// 会由调用方线程执行,这可能导致调用方线程被阻塞
pool.execute(() -> {
    pool.execute(() -> innerTask());  // 调用方也是线程池线程 → 死锁风险
});
💡

面试加分点:能说出"JDK 21 引入的虚拟线程(Virtual Threads)使得传统的线程池模式需要重新考虑——虚拟线程的栈是堆内存分配的,创建成本极低,传统的线程池可能被虚拟线程池(Executors.newVirtualThreadPerTaskExecutor())替代",说明他对 JDK 21 有跟进。

⚠️

面试陷阱:被问到"keepAliveTime 设为 0 是什么意思",很多人会说"立即回收"。准确答案是:设为 0 表示非核心线程在执行完任务后立即被回收。如果 allowCoreThreadTimeOut = true,核心线程也会立即被回收。