一、 为什么必须使用 java线程池?

在Java后端开发面试中,java线程池面试题库几乎占据了并发编程板块的半壁江山。许多初学者认为 new Thread() 简单直观,但在生产环境中,直接创建线程是极其危险且低效的。线程池的核心价值在于:资源复用、控制并发数、提高响应速度。

1.1 降低资源消耗

通过重复利用已创建的线程,降低线程创建和销毁造成的损耗。每次 new Thread() 都会触发操作系统的上下文切换,这是一个昂贵的操作。

1.2 提高响应速度

当任务到达时,如果线程已经存在,就可以立即执行,无需等待线程创建。

1.3 提高线程的可管理性

线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。

对比维度 new Thread() ExecutorService (线程池)
资源消耗 高(频繁创建销毁) 低(复用线程)
稳定性 低(易OOM) 高(可控并发)
可维护性 差 好(统一监控)

二、 ThreadPoolExecutor 七大核心参数

这是 java线程池面试题库中最基础也最重要的部分。面试官通常会要求手写一个线程池,或者解释每个参数的含义。

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

① corePoolSize (核心线程数)

线程池中的核心线程数。默认情况下,核心线程会一直存活,即使处于闲置状态也不会被销毁(除非设置了 allowCoreThreadTimeOut)。当有新任务到来时,如果当前线程数小于核心线程数,则会创建新线程执行任务。

② maximumPoolSize (最大线程数)

线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则会创建新的线程执行任务。注意:如果使用了无界队列,这个参数就没什么效果了。

③ keepAliveTime & ④ unit

非核心线程闲置时的超时时长。超过这个时长,非核心线程就会被回收。当线程池中的线程数量超过 corePoolSize 时,如果等待时间超过 keepAliveTime,则多余的空闲线程会被终止。

⑤ workQueue (工作队列)

用于传输和保存等待执行任务的阻塞队列。常见的有 ArrayBlockingQueue, LinkedBlockingQueue, SynchronousQueue 等。

⑥ threadFactory (线程工厂)

用于创建新线程的工厂。可以通过线程工厂给每个创建出来的线程设置更有意义的名字,方便排查问题。推荐使用 Executors.defaultThreadFactory() 或自定义工厂添加前缀。

⑦ handler (拒绝策略)

当队列和最大线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。默认是 AbortPolicy,直接抛出异常。

三、 工作队列(BlockingQueue)的选择与陷阱

在 java线程池面试题库中,关于队列的考察往往伴随着“线程池状态流转”的问题。不同的队列会导致线程池的行为发生巨大变化。

3.1 常见队列对比

  • ArrayBlockingQueue:基于数组的有界阻塞队列。按FIFO排序任务,构造函数必须指定容量。适用于资源受限的系统。
  • LinkedBlockingQueue:基于链表的无界(或有限界)阻塞队列。吞吐量通常高于ArrayBlockingQueue。如果不指定容量,默认是 Integer.MAX_VALUE,这会导致内存溢出风险。
  • SynchronousQueue:不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态。通常用于直接移交任务的场景,如 Executors.newCachedThreadPool()。
  • PriorityBlockingQueue:具有优先级的无界阻塞队列。

3.2 面试陷阱:LinkedBlockingQueue 的容量

很多开发者直接使用 new ThreadPoolExecutor(..., new LinkedBlockingQueue(), ...)。如果不指定容量,默认大小为 2^31-1。这意味着队列几乎永远不会满,那么 maximumPoolSize 参数也就失去了意义,线程池实际上退化为了只有核心线程数的固定大小线程池,且存在巨大的内存OOM风险。

// 错误示例
new ThreadPoolExecutor(
    2,
    10,
    60L, TimeUnit.SECONDS,
    new LinkedBlockingQueue<Runnable>() // 容量默认 2^31-1
);
// 正确示例
new ThreadPoolExecutor(
    2,
    10,
    60L, TimeUnit.SECONDS,
    new LinkedBlockingQueue<Runnable>(100) // 指定合理容量
);

四、 拒绝策略(RejectedExecutionHandler)

当线程池和队列都满了,新来的任务该如何处理?JDK提供了四种内置策略,也支持自定义。

策略一:AbortPolicy(默认)

直接抛出 RejectedExecutionException 异常,阻止系统正常运行。这是最严格的策略,适合对数据完整性要求极高的场景。

策略二:CallerRunsPolicy

由调用线程处理该任务。即提交任务的线程自己去执行这个任务。这会在一定程度上降低提交任务的速率,从而给线程池缓冲的时间,是一种“反压”机制。

策略三:DiscardPolicy

直接丢弃任务,没有任何异常抛出。适用于对任务丢失不敏感的场景,如日志收集。

策略四:DiscardOldestPolicy

丢弃队列中最老的任务,即下一个将被执行的任务,然后尝试重新提交新的任务。这可能导致重要任务被误删,需谨慎使用。

4.1 自定义拒绝策略

在实际项目中,我们通常不会直接使用JDK内置策略,而是实现 RejectedExecutionHandler 接口,将拒绝的任务记录到日志、存入数据库或发送到MQ中,保证任务不丢失。

五、 线程池的生命周期与运行状态

理解 ThreadPoolExecutor 的状态是深入源码的关键。线程池的状态信息被封装在 runState 变量中,它与工作线程数量共用一个 AtomicInteger 进行存储,状态在前5位,线程数在后27位。

5.1 五种运行状态

  • RUNNING:接受新任务,处理排队任务。
  • SHUTDOWN:不接受新任务,但处理排队任务。调用 shutdown() 进入此状态。
  • STOP:不接受新任务,不处理排队任务,并中断正在处理的任务。调用 shutdownNow() 进入此状态。
  • TIDYING:所有的任务都已终止,workerCount 为0。线程过渡到该状态将运行 terminated() 钩子方法。
  • TERMINATED:terminated() 方法已完成。
// 状态转换图
RUNNING  --shutdown()-->  SHUTDOWN
RUNNING  --shutdownNow()--> STOP
SHUTDOWN --drainQueue()--> TIDYING
STOP     --drainQueue()--> TIDYING
TIDYING  --terminated()--> TERMINATED

六、 阿里巴巴Java开发手册与最佳实践

在 java线程池面试题库中,面试官非常看重候选人是否遵循团队规范。《阿里巴巴Java开发手册》明确规定:线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

💡 核心线程数如何设置?

这取决于任务是 CPU密集型 还是 IO密集型。

  • CPU密集型:需要充分发挥CPU的性能,减少上下文切换。一般设置为 N+1(N为CPU核数)。
  • IO密集型:大部分时间都在等待IO,CPU利用率低。一般设置为 2N 或者 N / (1 - 阻塞系数)。阻塞系数通常在0.8~0.9之间。
  • 混合型:可以根据任务执行时间的比例进行拆分。

6.1 完整配置示例

public class ThreadPoolConfig {
    public static ExecutorService createThreadPool() {
        return new ThreadPoolExecutor(
            5,                           // 核心线程数
            10,                          // 最大线程数
            60L, TimeUnit.SECONDS,       // 空闲超时
            new ArrayBlockingQueue<Runnable>(500), // 有界队列
            new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build(), // 命名线程
            new ThreadPoolExecutor.CallerRunsPolicy() // 调用者运行策略
        );
    }
}

七、 网友们还关心:高频面试题深度解答

除了基础原理,java线程池面试题库中还有很多场景题和进阶问题。以下是网友们搜索频率最高的几个问题。

Q1: 线程池满了会发生什么?

当线程池达到 maximumPoolSize 且工作队列 workQueue 也满了时,新提交的任务会触发拒绝策略。默认情况下会抛出 RejectedExecutionException。如果使用了 CallerRunsPolicy,则由提交任务的线程自己执行。在实际生产中,必须捕获或处理这个异常,否则可能导致业务逻辑中断。

Q2: 如何监控线程池?

可以通过继承 ThreadPoolExecutor 并重写 beforeExecute, afterExecute, terminated 方法来监控线程执行情况。结合 ActiveMQ 或 Prometheus + Grafana 可以实时展示线程池的核心指标:当前线程数、活跃线程数、队列大小、已完成任务数等。这对于排查线上性能问题至关重要。

Q3: submit 和 execute 区别?

execute() 是 Executor 接口的方法,只接受 Runnable 参数,不返回结果,无法捕获异常。 submit() 是 ExecutorService 接口的方法,可以接受 Runnable 或 Callable,返回 Future 对象,可以通过 future.get() 获取执行结果或捕获异常。在 java线程池面试题库中,这是一个经典的细节考点。

八、 周边知识拓展:Fork/Join 与 CompletableFuture

在掌握了基础的 java线程池面试题库知识后,进一步探索并发工具包(JUC)中的高级组件是进阶必经之路。

8.1 Fork/Join 框架

Fork/Join 框架是 JDK 7 引入的,用于并行执行任务。它采用了“工作窃取”(Work-Stealing)算法。当一个线程处理完自己的任务队列后,会从其他忙碌线程的队列尾部窃取任务来执行,从而最大化CPU利用率。它特别适合处理可以拆分的递归任务,如大规模数组求和、图像渲染等。

8.2 CompletableFuture

CompletableFuture 是 Java 8 引入的,用于简化异步编程。它结合了 Future 和 Callback 的优点,支持链式调用、异常处理、组合多个异步任务等。在微服务架构中,经常使用 CompletableFuture 来并行调用多个下游服务,最后汇总结果,极大提升接口响应速度。