【面试突击四】JAVA基础知识-线程池与参数调优

文章目录

  • 【面试突击】JAVA基础知识-线程池与参数调优
    • 一、为什么要用线程池?
    • [二、`ThreadPoolExecutor` 核心参数详解](#二、ThreadPoolExecutor 核心参数详解)
      • [1. `corePoolSize`:核心线程数](#1. corePoolSize:核心线程数)
      • [2. `maximumPoolSize`:最大线程数](#2. maximumPoolSize:最大线程数)
      • [3. `keepAliveTime` + `unit`:线程空闲存活时间](#3. keepAliveTime + unit:线程空闲存活时间)
      • [4. `workQueue`:任务队列(非常关键)](#4. workQueue:任务队列(非常关键))
      • [5. `threadFactory`:线程工厂](#5. threadFactory:线程工厂)
      • [6. `RejectedExecutionHandler`:拒绝策略](#6. RejectedExecutionHandler:拒绝策略)
    • [三、队列选择:`ArrayBlockingQueue` vs `LinkedBlockingQueue` vs `SynchronousQueue`](#三、队列选择:ArrayBlockingQueue vs LinkedBlockingQueue vs SynchronousQueue)
      • [1. `ArrayBlockingQueue`:数组有界阻塞队列](#1. ArrayBlockingQueue:数组有界阻塞队列)
      • [2. `LinkedBlockingQueue`:链表阻塞队列(默认近似无界)](#2. LinkedBlockingQueue:链表阻塞队列(默认近似无界))
      • [3. `SynchronousQueue`:零容量队列](#3. SynchronousQueue:零容量队列)
    • 四、拒绝策略:`RejectedExecutionHandler`
      • [1. `AbortPolicy`(默认)------ 抛异常](#1. AbortPolicy(默认)—— 抛异常)
      • [2. `CallerRunsPolicy`------ 由提交线程自己执行](#2. CallerRunsPolicy—— 由提交线程自己执行)
      • [3. `DiscardPolicy`------ 直接丢弃当前任务](#3. DiscardPolicy—— 直接丢弃当前任务)
      • [4. `DiscardOldestPolicy`------ 丢队头旧任务,保新任务](#4. DiscardOldestPolicy—— 丢队头旧任务,保新任务)
    • [五、为什么不推荐直接使用 `Executors` 系列工厂方法?](#五、为什么不推荐直接使用 Executors 系列工厂方法?)
      • [1. `newFixedThreadPool` / `newSingleThreadExecutor`](#1. newFixedThreadPool / newSingleThreadExecutor)
      • [2. `newCachedThreadPool`](#2. newCachedThreadPool)
      • [3. 官方 & 规范建议](#3. 官方 & 规范建议)
    • 六、推荐的线程池创建方式(模板)
    • [七、ArrayBlockingQueue 与 LinkedBlockingQueue 的简单对比(面试可用)](#七、ArrayBlockingQueue 与 LinkedBlockingQueue 的简单对比(面试可用))
    • 八、面试速记总结

【面试突击】JAVA基础知识-线程池与参数调优

本文聚焦:ThreadPoolExecutor 参数含义、队列选择、拒绝策略,以及为什么不推荐直接用 Executors 系列工厂方法


一、为什么要用线程池?

  • 线程创建/销毁成本高:涉及系统调用、栈内存分配、调度开销;
  • 频繁创建新线程,容易造成:
    • 线程数失控(成百上千个线程导致频繁上下文切换);
    • 内存压力增大;
  • 使用线程池可以:
    • 复用线程,减少创建/销毁开销;
    • 控制最大并发数,起到"限流阀门"的作用;
    • 提供任务排队、拒绝策略等机制,提升系统稳定性。

Java 中线程池的核心类是:java.util.concurrent.ThreadPoolExecutor


二、ThreadPoolExecutor 核心参数详解

关键构造方法:

java 复制代码
public ThreadPoolExecutor(
        int corePoolSize,          // 核心线程数
        int maximumPoolSize,       // 最大线程数
        long keepAliveTime,        // 非核心线程闲置存活时间
        TimeUnit unit,             // 上述时间单位
        BlockingQueue<Runnable> workQueue,  // 任务队列
        ThreadFactory threadFactory,        // 线程工厂
        RejectedExecutionHandler handler    // 拒绝策略
)

下面逐个讲解。

1. corePoolSize:核心线程数

  • 线程池会尽量维持这么多线程长期存在(除非设置了允许核心线程超时)。
  • 当有新任务到达时:
    • 当前线程数 < corePoolSize → 直接创建新线程执行(即便队列有空间);
    • 当前线程数 ≥ corePoolSize → 任务先尝试入队。

经验值:

  • CPU 密集型任务:corePoolSize ≈ CPU 核数N+1
  • IO 密集型任务:corePoolSize = CPU 核数 * 2 甚至更多,根据 IO 阻塞比例调优。

2. maximumPoolSize:最大线程数

  • 线程池允许创建的最大线程数
  • 触发扩容的条件:
    • 当前线程数 ≥ corePoolSize
    • 队列已满;
    • 此时如果线程数 < maximumPoolSize,则继续创建新线程执行任务。

注意:如果使用的是无界队列 (如默认的 LinkedBlockingQueue),队列基本不会满,maximumPoolSize 几乎不会生效。

3. keepAliveTime + unit:线程空闲存活时间

  • 当线程数 > corePoolSize 时,多出来的线程如果空闲时间超过 keepAliveTime 会被回收。
  • 如果调用:allowCoreThreadTimeOut(true),核心线程空闲也会被回收。

适合负载波动大的场景(高峰扩容,低谷回收)。

4. workQueue:任务队列(非常关键)

  • 用于缓冲提交但是暂时没有可用线程执行的任务;
  • 不同队列类型直接决定线程池的扩容、排队、拒绝行为;
  • 常见选择:
    • ArrayBlockingQueue
    • LinkedBlockingQueue
    • SynchronousQueue

具体对比见后文"队列选择"。

5. threadFactory:线程工厂

  • 用于创建新线程;
  • 一般用于:
    • 自定义线程名称(便于定位问题);
    • 设置为守护/非守护线程;
    • 设置优先级等。
java 复制代码
ThreadFactory factory = r -> {
    Thread t = new Thread(r);
    t.setName("biz-worker-" + t.getId());
    return t;
};

线上强烈建议自定义线程名,日志/监控会友好很多。

6. RejectedExecutionHandler:拒绝策略

  • 队列已满 & 线程数已达 maximumPoolSize 时,再提交新任务会触发;
  • 它定义了:线程池过载时,如何处理新任务;
  • 详见"拒绝策略"一节。

三、队列选择:ArrayBlockingQueue vs LinkedBlockingQueue vs SynchronousQueue

workQueueBlockingQueue<Runnable>,不同实现差异很大。

1. ArrayBlockingQueue:数组有界阻塞队列

java 复制代码
BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1000);

特性:

  • 必须指定容量,严格有界
  • 底层用数组,内存更紧凑,可预测性强;
  • 入队/出队共享一把锁(经典实现),高并发下锁竞争略大;
  • 可选公平性参数:new ArrayBlockingQueue<>(capacity, fair)

适用场景:

  • 线程池任务队列推荐
  • 你希望:
    • 限制排队任务数(避免 OOM);
    • 排队上限清晰易控;
  • 内存敏感、SLA 明确的系统,比如 Web 服务请求处理线程池。

2. LinkedBlockingQueue:链表阻塞队列(默认近似无界)

java 复制代码
// 默认,无界(容量是 Integer.MAX_VALUE)
BlockingQueue<Runnable> q1 = new LinkedBlockingQueue<>();

// 有界形式
BlockingQueue<Runnable> q2 = new LinkedBlockingQueue<>(1000);

特性:

  • 不指定容量时,近似无界
  • 底层链表,每个元素一个 Node 对象,内存占用相对更大;
  • 内部使用两把锁(putLock / takeLock),生产和消费大多可并行,吞吐量通常优于 ArrayBlockingQueue

适用场景:

  • 一般生产者-消费者模型,对吞吐有要求;
  • 用在线程池时一定要指定容量,否则任务会无限堆积,最终 OOM 或超长延迟。

3. SynchronousQueue:零容量队列

java 复制代码
BlockingQueue<Runnable> queue = new SynchronousQueue<>();

特性:

  • 容量为 0,不存任务;
  • 每次 put 必须等待有线程 take,是一种"任务直接交给线程"的模式;
  • 适合吞吐高、任务非常短的场景。

配合线程池行为:

  • 队列不会存任务,因此:
    • 当前线程数 < maximumPoolSize 时,会不断新建线程执行任务;
    • 达到 maximumPoolSize 后,新的任务无法入队也无法扩容 → 触发拒绝策略。

JDK 内置的 Executors.newCachedThreadPool() 就是用的 SynchronousQueue


四、拒绝策略:RejectedExecutionHandler

触发条件:线程数达到 maximumPoolSize 且队列已满

JDK 提供了 4 种常用策略:

1. AbortPolicy(默认)------ 抛异常

java 复制代码
new ThreadPoolExecutor.AbortPolicy()

行为:

  • 直接抛出 RejectedExecutionException
  • 如果上层没捕获,任务提交方会感知到异常。

适合:

  • 希望显式暴露问题,调试/测试阶段尤其好用;
  • 生产中也常配合上层快速失败 + 监控报警。

2. CallerRunsPolicy------ 由提交线程自己执行

java 复制代码
new ThreadPoolExecutor.CallerRunsPolicy()

行为:

  • 任务在提交任务的线程中执行;
  • 不抛异常、不丢任务,但提交方会被"拖慢"。

效果:

  • 一种自然限流 机制:
    • 当线程池忙不过来时,提交线程被迫参与执行任务;
    • 从源头减缓提交速率。

适合:

  • 可以接受响应变慢,但不希望随意丢任务的场景。

3. DiscardPolicy------ 直接丢弃当前任务

java 复制代码
new ThreadPoolExecutor.DiscardPolicy()

行为:

  • 静默丢弃新提交的任务,不抛异常。

适用场景非常有限,只在"任务可有可无"并且"可以容忍丢"时才考虑使用。

4. DiscardOldestPolicy------ 丢队头旧任务,保新任务

java 复制代码
new ThreadPoolExecutor.DiscardOldestPolicy()

行为:

  • 丢弃队列中最旧的任务(队头),然后尝试入队当前任务;
  • 同样不抛异常。

适合:

  • "新任务比旧任务更有价值"的业务场景,比如频繁状态上报,只关心最新状态。

五、为什么不推荐直接使用 Executors 系列工厂方法?

常见工厂方法:

  • Executors.newFixedThreadPool(int nThreads)
  • Executors.newSingleThreadExecutor()
  • Executors.newCachedThreadPool()
  • Executors.newScheduledThreadPool(int corePoolSize)

问题主要在于:隐藏了关键参数,默认策略不安全

1. newFixedThreadPool / newSingleThreadExecutor

内部实现简化后大致如下:

java 复制代码
// newFixedThreadPool
return new ThreadPoolExecutor(
        nThreads, nThreads,
        0L, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<Runnable>()); // 无界队列

// newSingleThreadExecutor
return new FinalizableDelegatedExecutorService(
        new ThreadPoolExecutor(
            1, 1,
            0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>())); // 无界队列

问题:

  • 使用的是无界队列 LinkedBlockingQueue(容量 = Integer.MAX_VALUE);
  • 当线程数固定后,多余任务全部排队:
    • 生产速度 > 消费速度 → 队列无限增长;
    • 极易导致 OOM 或者极长延迟(任务排队排到"天荒地老")。

2. newCachedThreadPool

内部大致实现:

java 复制代码
return new ThreadPoolExecutor(
        0, Integer.MAX_VALUE,
        60L, TimeUnit.SECONDS,
        new SynchronousQueue<Runnable>());

问题:

  • 无界最大线程数(Integer.MAX_VALUE);
  • 队列为 SynchronousQueue,不存任务,提交一个来一个线程;
  • 短时间大量请求时,会疯狂创建新线程:
    • 几千、几万个线程都不是问题;
    • 非常容易把机器资源打满(上下文切换飙升、内存耗尽)。

3. 官方 & 规范建议

包括阿里巴巴 Java 开发手册在内的多家规范,都明确强调:

不要直接使用 Executors 提供的快捷线程池工厂,

需要使用 ThreadPoolExecutor 手动创建线程池,显式指定参数。


六、推荐的线程池创建方式(模板)

一个比较通用的 Web 服务线程池模板:

java 复制代码
import java.util.concurrent.*;

public class ThreadPoolConfig {

    public static ExecutorService buildBizExecutor() {
        int core = Runtime.getRuntime().availableProcessors();
        int max = core * 2;
        int queueCapacity = 1000;
        long keepAlive = 60L;

        ThreadFactory factory = r -> {
            Thread t = new Thread(r);
            t.setName("biz-exec-" + t.getId());
            // t.setDaemon(false);
            return t;
        };

        return new ThreadPoolExecutor(
                core,
                max,
                keepAlive,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(queueCapacity),
                factory,
                new ThreadPoolExecutor.CallerRunsPolicy()
        );
    }
}

特点:

  • 核心线程数 ≈ CPU 核数;
  • 有界 ArrayBlockingQueue 限制队列长度,防止任务无限堆积;
  • 最大线程数为核心的 2 倍,应对突发流量;
  • 使用 CallerRunsPolicy 做自然限流。

七、ArrayBlockingQueue 与 LinkedBlockingQueue 的简单对比(面试可用)

问:在线程池里 ArrayBlockingQueue 和 LinkedBlockingQueue 有什么区别,使用场景?

回答要点:

  1. 结构与容量:
    • ArrayBlockingQueue:数组实现,必须指定容量,是严格有界队列,内存更紧凑;
    • LinkedBlockingQueue:链表实现,不指定容量时近似无界,也可以指定容量变有界。
  2. 并发特性:
    • ArrayBlockingQueue:入队/出队用一把锁,高并发下锁竞争略大;
    • LinkedBlockingQueue:入队/出队两把锁,生产/消费可高度并行,整体吞吐更好。
  3. 使用建议:
    • 在线程池里,优先用有界队列ArrayBlockingQueueLinkedBlockingQueue(capacity)),防止任务堆积 OOM;
    • 一般的生产者-消费者队列、需要更高吞吐时,可以优先用 LinkedBlockingQueue,同样建议有限容量。

八、面试速记总结

  1. 线程池推荐使用 ThreadPoolExecutor 手动创建,不要直接用 Executors 工厂方法
  2. 核心参数:
    • corePoolSize:常驻线程数;
    • maximumPoolSize:最大线程数,配合有界队列才有意义;
    • workQueue:任务队列,优先有界;
    • RejectedExecutionHandler:过载保护策略。
  3. 队列选择:
    • ArrayBlockingQueue:有界、数组、内存可控,适合线程池任务队列;
    • LinkedBlockingQueue:链表、默认无界,注意避免在线程池中无限堆积任务。
  4. 拒绝策略:
    • 默认 AbortPolicy:抛异常;
    • 常用 CallerRunsPolicy:提交线程自己执行,形成自然限流;
    • 其他两种丢弃策略慎用。
  5. Executors 不推荐原因:
    • newFixedThreadPool & newSingleThreadExecutor:无界队列,易 OOM;
    • newCachedThreadPool:最大线程数几乎无限,易创建过多线程压垮系统。

相关推荐
小股虫2 小时前
Tair Java实操手册:从零开始的缓存中间件入门指南
java·缓存·中间件
Wyy_9527*2 小时前
Spring三种注入方式对比
java·后端·spring
shepherd1112 小时前
从入门到实践:玩转分布式链路追踪利器SkyWalking
java·后端·架构
最贪吃的虎2 小时前
网络是怎么传输的:从底层协议到浏览器访问网站的全过程剖析
java·开发语言·网络·http·缓存
uup2 小时前
CompletableFuture 异常吞噬:异步任务异常未处理导致结果丢失
java
NAGNIP2 小时前
Kimi Linear——有望替代全注意力的全新注意力架构
算法·面试
有一个好名字2 小时前
设计模式-工厂方法模式
java·设计模式·工厂方法模式
篱笆院的狗2 小时前
Java 中线程之间如何进行通信?
java·开发语言
专业IT有讠果2 小时前
[Docker/K8S] Kubernetes故障克星:19个高频问题速查与秒解指南(2025版)
javascript·面试