1.介绍一下线程池的工作原理

线程池(java.util.concurrent.ThreadPoolExecutor)用于复用线程、控制并发度、管理任务队列和生命周期,从而提升性能并避免频繁创建/销毁线程的开销。

img

线程池分为核心线程池线程池的最大容量等待任务的队列,提交一个任务,如果核心线程没有满,就创建一个线程,如果满了,就是会加入等待队列,如果等待队列满了,就会增加线程,如果都达到最大线程数量,就会按照一些丢弃的策略进行处理。

任务执行流程如下:

提交任务 → 核心线程是否已满?
  ├─ 未满 → 创建核心线程执行
  └─ 已满 → 任务入队
       ├─ 队列未满 → 等待执行
       └─ 队列已满 → 创建非核心线程
           ├─ 未达最大线程数 → 执行任务
           └─ 已达最大线程数 → 执行拒绝策略

2.线程池的参数有哪些?

  • corePoolSize:核心线程数(优先创建到这个数量)。

  • maximumPoolSize:最大线程数。

  • keepAliveTime + TimeUnit:超时时间(用于回收空闲线程)。

  • workQueue(BlockingQueue<Runnable>):任务队列类型(决定阻塞/扩展策略)。

  • threadFactory:自定义线程创建(命名/daemon/异常处理)。

  • handler(RejectedExecutionHandler):拒绝策略(超出容量时如何处理)。

  • 其他:allowCoreThreadTimeOut(boolean)prestartCoreThread() / prestartAllCoreThreads()remove(Runnable) 等。

3.常见队列类型与影响

  • SynchronousQueue:不保存任务,直接交给空闲线程(常用于 cached pool),会促使创建新线程,若无空闲线程且达到 max 则拒绝。

  • LinkedBlockingQueue(无界或指定容量):常见;若无界(默认)则队列永远能入队,maximumPoolSize 将不会生效。

  • ArrayBlockingQueue:有界环形队列,常用于限制队列大小。

  • PriorityBlockingQueue / DelayQueue:特殊语义(优先级/延迟执行)。

4.拒绝策略(内置四种)

  • AbortPolicy(默认):抛出 RejectedExecutionException

  • CallerRunsPolicy:调用者线程自己执行该任务(提供退坡降级)。

  • DiscardOldestPolicy:丢弃队列头(最老)并尝试再次提交。

  • DiscardPolicy:静默丢弃新任务。

5.线程池常见实现与工厂

  • Executors.newFixedThreadPool(n) → core=n, max=n, unbounded LinkedBlockingQueue(注意:队列无界可能导致 OOM)。

  • Executors.newCachedThreadPool() → core=0, max=Integer.MAX_VALUE, SynchronousQueue, keepAlive=60s(适合短时并发突发)。

  • Executors.newSingleThreadExecutor() → single thread + unbounded队列。
    (面试中强调:不要盲用 Executors 的工厂方法,推荐显式创建 ThreadPoolExecutor 并指定有界队列与拒绝策略。)

6.如何调优线程池

  • 线程数估算:

    • CPU 密集型:线程数 ≈ CPU 核心数(或 cores ±1)。

    • IO/阻塞型:线程数 ≈ cores * (1 + waitTime / serviceTime)。

  • 使用有界队列以避免无限制内存增长。

  • 选合适的队列:对延迟敏感用 SynchronousQueue(配合合理 max),对吞吐用有界 LinkedBlockingQueue。

  • 配置 ThreadFactory(线程名/daemon/uncaughtExceptionHandler)便于监控与排错。

  • 设置合适的拒绝策略并监测队列长度、活跃线程数、任务完成率等指标。

  • 在任务结束或应用关闭时显式 shutdown(),避免 JVM 无法退出或线程泄漏。

常见坑
  • 使用 newFixedThreadPoolnewSingleThreadExecutor 的无界队列可能导致 OOM。

  • 在线程池中提交长期阻塞任务,会耗尽线程导致吞吐下降或死锁(例如任务互相等待)。

  • 不关闭线程池(忘记 shutdown())导致应用无法优雅退出。

  • 不了解队列类型导致 maximumPoolSize 被忽略(无界队列情形)。

  • 忽略任务抛异常的处理(可用 afterExecute / 自定义 ThreadFactory 捕获)。

小示例
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    corePoolSize, maximumPoolSize,
    keepAliveTime, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(queueCapacity),
    myThreadFactory,
    new ThreadPoolExecutor.CallerRunsPolicy());

说明:先填满 core,再入队;队满且线程数 < max 则扩容;否则触发拒绝策略。

30 秒逐字稿

“线程池通过复用线程来执行提交的任务:提交时先创建到 corePoolSize,满后任务入队;队列满且当前线程数 < maximumPoolSize 会再创建线程,否则触发拒绝策略。关键参数包括 corePoolSizemaximumPoolSizekeepAliveTimeworkQueuethreadFactoryRejectedExecutionHandler。要注意队列选择(无界队列会忽略 max)、合理设置线程数(CPU-bound vs IO-bound)、使用有界队列并实现合适的拒绝和关闭策略,避免 OOM、任务串行化或线程泄漏。”

7.线程池中shutdown (),shutdownNow()这两个方法有什么作用?提交给线程池中的任务可以被撤回吗?

一句话概述?

shutdown() 是“平滑关机”——不接受新任务,等待已提交(正在执行 + 队列中)的任务执行完;
shutdownNow() 是“立即尝试停止”——不接受新任务,尝试中断正在运行的线程,并返回尚未开始执行的排队任务列表,但不能保证已运行的任务马上停止

两个方法的具体行为对比?
  • shutdown()

    • 标记线程池进入 SHUTDOWN 状态(不再接受新任务,提交会抛 RejectedExecutionException)。

    • 已提交的任务(正在运行或在队列中)会继续执行直到完成或被取消。

    • 不会中断正在执行的任务(除非任务自己检查到某种条件并退出)。

  • shutdownNow()

    • 将线程池标记为 STOP(不接受新任务)。

    • 尝试中断所有正在运行的 worker 线程(调用 Thread.interrupt())。

    • 将阻塞队列中尚未开始的任务移除并以 List<Runnable> 返回给调用者。

    • 注意:中断是“协作式”的——只有任务响应中断(抛出 InterruptedException 或检查 Thread.currentThread().isInterrupted() 并退出)时才能被真正终止;否则它会继续运行。

  • 共同点:两者都不会强制立刻终止不响应中断的任务,也不会使用不安全的 Thread.stop()

提交给线程池的任务能否被撤回?(分情况说明)
  1. 还在队列中(尚未开始执行)

    • shutdownNow() 会把这些任务从队列中取出并返回(已撤回)。

    • 也可以在任意时刻调用 executor.getQueue().remove(runnable)executor.remove(runnable) 来显式从队列中移除某个 Runnable(需持有该 runnable 的引用)。

    • 如果任务是通过 submit() 提交的,返回的是 Future,可调用 future.cancel(false)(不中断)或 future.cancel(true)(如果还没开始,取消并从队列移除;如果已开始则尝试中断运行线程,是否生效取决于任务)。

  2. 已开始执行(正在运行)

    • 不能被强制“立即”撤回。可以调用 future.cancel(true)shutdownNow() 发起对线程的中断请求:这会设置中断标志并(对于阻塞调用或抛出 InterruptedException 的点)导致任务返回/退出。

    • 如果任务不检查中断且不抛 InterruptedException(例如在长计算循环里没有检查中断),那么中断不会让任务停止——需要任务代码主动响应中断或提供自定义取消机制(如检查某个 volatile 标志)。

  3. 任务以 execute(Runnable) 提交且没有 Future

    • 只能通过 executor.getQueue().remove(runnable)(在尚未开始)或 shutdownNow() 来移除;没有 Future 就不能通过 cancel 操作。

    • 如果需要可取消的任务,建议使用 submit()(得到 Future)或手动包装 FutureTask 并保存引用以便 later cancel/remove。

常见补充点?
  • shutdownNow() 返回的 List<Runnable>还没开始执行的那些任务的引用,你可以选择重试提交、记录日志或处理它们。

  • 调用 shutdown() / shutdownNow() 后,可以调用 awaitTermination(timeout, unit) 等待线程池完全终止。

  • 对于需要确定性取消的任务,设计任务为可中断(在阻塞点或循环中频繁检查 Thread.interrupted() 或抛 InterruptedException)是最佳做法。

  • 不要使用已废弃且不安全的 Thread.stop() 来强制结束任务(面试时可以明确拒绝这种方式并解释原因)。

典型优雅关闭模板?
executor.shutdown(); // 不再接受新任务
try {
    if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
        // 超时后尝试立即停止
        List<Runnable> dropped = executor.shutdownNow(); // 返回尚未开始的任务
        // 可记录或重新提交 dropped
        if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
            System.err.println("Pool did not terminate");
        }
    }
} catch (InterruptedException ie) {
    // 当前线程被中断时,也尝试停止并恢复中断状态
    List<Runnable> dropped = executor.shutdownNow();
    Thread.currentThread().interrupt();
}
如何设计任务以便能被撤回?
  • 可中断阻塞:使用可中断的阻塞 API(BlockingQueue.take()Thread.sleep()SocketChannel 的 interruptible I/O 等),在 catch InterruptedException 时清理并返回。

  • 检查中断标志:在长计算循环中周期性检查 Thread.currentThread().isInterrupted() 并优雅退出。

  • 使用 Future:用 submit() 得到 Future,在需要时调用 cancel(true)

  • 在队列中可移除:如果任务可能被撤回,保留任务的引用以便 remove()

  • 实现自定义取消逻辑(比如 volatile boolean cancelled),并在任务入口/循环中检查。


面试 30 秒逐字稿

shutdown() 是平滑关机:不接受新任务,等待队列和正在运行的任务完成;shutdownNow() 是立即尝试停止:不接受新任务,会中断正在运行的线程并返回队列中尚未执行的任务列表。但 shutdownNow() 只能发出中断请求,不能保证正在执行的任务马上停止——任务必须是可中断或自行检查中断/取消标志。想撤回任务:若任务还在队列中可以通过 executor.remove(runnable)shutdownNow() 获取并处理它;若任务已开始,应该通过 Future.cancel(true) 发出中断请求并让任务自己响应中断。”

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐