java之线程池

4 阅读5分钟

在 Java 中,java.util.concurrent.ThreadPoolExecutor 是线程池的核心实现类。掌握其参数配置与运行原理,是应对大厂面试的必备技能。下面从参数详解、工作流程、源码关键逻辑、以及实践配置建议几个方面展开。


一、核心参数详解

java

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)

1. corePoolSize(核心线程数)

  • 线程池中保持存活的线程数量,即使空闲也不会被回收(除非设置了 allowCoreThreadTimeOut)。
  • 当提交任务时,如果当前线程数 < corePoolSize,会直接创建新线程执行任务(即使有空闲线程也优先创建新线程)。

2. maximumPoolSize(最大线程数)

  • 线程池允许创建的最大线程数。
  • 当任务队列满了且当前线程数 < maximumPoolSize 时,会继续创建新线程执行任务。

3. keepAliveTime(空闲存活时间)与 unit(时间单位)

  • 当线程数超过 corePoolSize 时,多余的空闲线程等待新任务的最长时间,超过此时间则被终止。
  • 如果设置了 allowCoreThreadTimeOut(true),则核心线程也会受此参数影响。

4. workQueue(任务队列)

  • 用于存放等待执行的任务的阻塞队列。

  • 常见队列:

    • SynchronousQueue:不存储任务,直接交给线程执行。如果没有空闲线程则创建新线程(常用于无界线程池)。
    • LinkedBlockingQueue:无界队列(默认容量 Integer.MAX_VALUE),可能导致内存溢出,且永远不会触发拒绝策略。
    • ArrayBlockingQueue:有界队列,可防止资源耗尽,需配合合理的 maximumPoolSize 使用。
    • PriorityBlockingQueue:支持优先级排序。

5. threadFactory(线程工厂)

  • 用于创建线程,可以自定义线程名称、是否为守护线程、优先级等,便于问题排查。

6. handler(拒绝策略)

  • 当线程池已关闭,或队列满且线程数已达 maximumPoolSize 时,新提交的任务会触发拒绝策略。

  • 内置策略:

    • AbortPolicy:直接抛出 RejectedExecutionException(默认)。
    • CallerRunsPolicy:由提交任务的线程(调用者)自己执行任务。
    • DiscardPolicy:静默丢弃任务,不抛出异常。
    • DiscardOldestPolicy:丢弃队列中最老的任务,然后重试提交当前任务。

二、线程池的工作原理(任务提交流程)

当调用 execute() 或 submit() 提交一个任务时,线程池的处理逻辑如下(以 ThreadPoolExecutor 源码为依据):

text

1. 如果当前运行的线程数 < corePoolSize:
      创建新线程执行任务(即使有其他空闲线程)。
2. 否则,尝试将任务加入工作队列:
      如果加入成功,等待核心线程或新增线程处理。
      注意:加入成功后需要双重检查线程池状态,如果线程池已停止则回滚。
3. 如果加入队列失败(队列已满):
      如果当前线程数 < maximumPoolSize:
          创建新线程执行任务。
      否则:
          触发拒绝策略。

关键源码片段(简化版):

java

public void execute(Runnable command) {
    int c = ctl.get();
    // 1. 当前线程数 < corePoolSize
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    // 2. 尝试入队
    if (isRunning(c) && workQueue.offer(command)) {
        // 再次检查状态,如果非运行状态则移除任务并拒绝
        if (!isRunning(ctl.get()) && remove(command))
            reject(command);
        // 如果当前线程数为0,则补充一个非核心线程
        else if (workerCountOf(ctl.get()) == 0)
            addWorker(null, false);
    }
    // 3. 队列满,尝试创建非核心线程
    else if (!addWorker(command, false))
        reject(command);
}

三、线程池的状态

线程池内部通过一个 AtomicIntegerctl)维护两个信息:

  • 高3位:线程池状态(RUNNINGSHUTDOWNSTOPTIDYINGTERMINATED
  • 低29位:工作线程数量

状态转换:

  • RUNNING:能接收新任务,并处理队列中的任务。
  • SHUTDOWN:不接收新任务,但能处理已提交的任务(调用 shutdown())。
  • STOP:不接收新任务,不处理队列中的任务,中断正在执行的任务(调用 shutdownNow())。
  • TIDYING:所有任务已终止,workerCount 为 0,将执行 terminated() 钩子方法。
  • TERMINATEDterminated() 执行完毕。

四、线程池的线程复用原理

线程池中的线程被封装为 Worker 对象,它继承自 AQS 并实现了 Runnable。每个 Worker 会循环从 workQueue 中获取任务并执行:

java

final void runWorker(Worker w) {
    Runnable task = w.firstTask;
    w.firstTask = null;
    while (task != null || (task = getTask()) != null) {
        // 执行任务前加锁(用于中断检测)
        task.run();
        task = null;
    }
    // 线程退出(空闲超时或被回收)
}

getTask() 方法中会根据当前线程数、超时设置等,决定是否从队列中阻塞/超时获取任务。如果超时且线程数超过 corePoolSize,则返回 null 导致线程退出。


五、参数配置实践建议

1. 区分任务类型

  • CPU 密集型:线程数一般设为 CPU 核数 + 1。核心线程数 = 最大线程数,队列可选有界队列(如 ArrayBlockingQueue)。
  • IO 密集型:线程数可以设大,比如 2 * CPU 核数。由于 IO 阻塞时间长,线程经常等待,可设置较多线程。

2. 合理使用队列

  • 无界队列(如 LinkedBlockingQueue :可能导致任务堆积过多,内存溢出。适用场景:任务量可控,或对响应时间要求不高。
  • 有界队列(如 ArrayBlockingQueue :配合最大线程数使用,可以触发拒绝策略,防止系统过载。

3. 动态调整

  • 美团等大厂在实践中有动态线程池配置方案:通过配置中心动态调整 corePoolSizemaximumPoolSize、队列容量,无需重启服务,以适应流量变化。

4. 拒绝策略选择

  • AbortPolicy:默认,直接抛异常,适合必须及时感知失败的业务。
  • CallerRunsPolicy:降级,由调用者线程执行,保证任务不丢,但可能阻塞调用方。
  • DiscardPolicy/DiscardOldestPolicy:适合允许丢失部分不重要的日志、监控数据等。

六、常见面试追问点

  • 线程池为什么先尝试创建核心线程,而不是先入队?
    这是为了提高响应速度,如果核心线程有空闲就直接执行,避免任务排队等待。
  • corePoolSize 为 0 会怎样?
    当 corePoolSize = 0 时,提交第一个任务会直接创建线程吗?源码中如果 corePoolSize = 0,则第一个任务不会立即创建线程,而是先尝试入队。若队列为空,则创建线程执行。最终线程池中会存在一个线程直到空闲超时。
  • 如何合理估算线程池大小?
    一般公式:线程数 = Ncpu * (1 + 平均等待时间 / 平均计算时间)。在实践中可通过压测逐步调整。