Java 线程池有几种创建方式?

面试考察点

  1. 基础掌握度:面试官不仅仅是想知道你会不会创建线程池,更是想知道你是否清楚 Java 并发包中线程池的分类和适用场景,能否根据业务特点选择合适的类型。

  2. 生产实践意识:考察你是否了解 Executors 工厂方法的潜在风险(无界队列导致 OOM、无限线程导致系统崩溃),以及为什么阿里开发手册明令禁止在生产环境使用。

  3. 原理理解深度:如果你只会调用 API 创建,但不理解线程池的执行流程(核心线程 → 队列 → 非核心线程 → 拒绝)和 7 个核心参数的含义,说明只是 “会用” 而非 “懂原理” 。

核心答案

Java 中创建线程池主要有 4 种方式:

创建方式核心类适用场景推荐指数
Executors 工厂方法 Executors 快速原型、测试 ⚠️ 禁止生产使用
手动构造 ThreadPoolExecutor 通用业务场景 ✅ 强烈推荐
定时任务 ScheduledThreadPoolExecutor 延迟/周期执行 ✅ 推荐
分治计算 ForkJoinPool 递归分解、并行计算 ✅ 特定场景

一句话总结:生产环境必须使用 ThreadPoolExecutor 手动构造,避免 Executors 的隐患。

深度解析

一、线程池执行流程

在介绍创建方式之前,必须先理解线程池的执行流程,否则参数配置就是 “盲人摸象” 。

线程池执行任务流程图线程池执行任务流程图

图示讲解:

上图展示了线程池执行任务的完整流程,整体分为 4 个阶段:

  1. 步骤 1 - 判断核心线程:当有新任务提交时,首先判断当前线程数是否小于核心线程数(corePoolSize)。如果是,则直接创建新的核心线程来执行任务,不需要排队。这个阶段是 “有人就干活” 。

  2. 步骤 2 - 加入队列:如果当前线程数已经达到核心线程数,新任务会尝试加入工作队列(workQueue)。队列起到了 “缓冲” 的作用,让任务先排队等待。

  3. 步骤 3 - 创建非核心线程:如果队列也满了,且当前线程数小于最大线程数(maximumPoolSize),则会创建非核心线程来执行任务。这是 “人手不够就招临时工” 的阶段。

  4. 步骤 4 - 执行拒绝策略:如果队列满了,线程数也达到最大值,就会执行拒绝策略。这是 “实在处理不了就拒绝” 的阶段。

关键点:线程池 不是 “先把线程创建满,再排队” ,而是按照 核心线程 → 队列 → 非核心线程 → 拒绝 的顺序处理任务。这个顺序很重要,它决定了队列的容量会直接影响何时创建非核心线程。

二、方式 1:Executors 工厂方法(⚠️ 生产环境禁止使用)

Executors 类提供了 4 种快捷创建方式,看起来很方便,但都存在严重隐患:

// ❌ 固定大小线程池
ExecutorService fixedPool = Executors.newFixedThreadPool(10);

// ❌ 缓存线程池
ExecutorService cachedPool = Executors.newCachedThreadPool();

// ❌ 单线程池
ExecutorService singlePool = Executors.newSingleThreadExecutor();

// 定时任务线程池
ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(5);

问题出在哪里?

Executors 为什么被禁止Executors 为什么被禁止

图示讲解:

上图展示了 Executors 两种主要工厂方法的内部实现问题:

  • newFixedThreadPool 和 newSingleThreadExecutor 的问题在于使用了 LinkedBlockingQueue,这是一个 无界队列(容量为 Integer.MAX_VALUE)。当任务提交速度超过处理速度时,队列会无限增长,最终导致内存溢出(OOM)。

  • newCachedThreadPool 的问题在于最大线程数设置为 Integer.MAX_VALUE,相当于 不限制线程数。配合容量为 0 的 SynchronousQueue,每个任务都会创建新线程。在高并发场景下,可能瞬间创建数万个线程,导致 CPU 飙升、系统崩溃。

阿里 Java 开发手册明确规定:线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,规避资源耗尽的风险。

三、方式 2:ThreadPoolExecutor 手动构造(✅ 强烈推荐)

这是生产环境的正确姿势,7 个参数完全由你掌控:

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    // ① corePoolSize:核心线程数(常驻线程,即使空闲也不会被回收)
    5,

    // ② maximumPoolSize:最大线程数 = 核心线程 + 非核心线程
    10,

    // ③ keepAliveTime:非核心线程空闲后的存活时间
    60L,

    // ④ unit:存活时间单位
    TimeUnit.SECONDS,

    // ⑤ workQueue:任务队列(⚠️ 必须有界!)
    new ArrayBlockingQueue<>(100),

    // ⑥ threadFactory:线程工厂(自定义线程名,方便排查问题)
    r -> {
        Thread t = new Thread(r, “my-pool-“ + new AtomicInteger(1).getAndIncrement());
        t.setDaemon(false);  // 用户线程,防止 JVM 提前退出
        return t;
    },

    // ⑦ handler:拒绝策略
    new ThreadPoolExecutor.CallerRunsPolicy()
);

7 个核心参数详解:

参数含义配置建议
corePoolSize 核心线程数(常驻,不回收) CPU 密集型:N + 1;IO 密集型:2N 或更高
maximumPoolSize 最大线程数(核心 + 非核心) 不宜过大,一般为核心线程的 1.5~2 倍
keepAliveTime 非核心线程空闲存活时间 一般 60 秒足够,太短会频繁创建销毁
unit 时间单位 TimeUnit.SECONDS
workQueue 任务队列 ⚠️ 必须有界! 推荐 ArrayBlockingQueue
threadFactory 线程工厂 务必自定义线程名,方便问题排查和监控
handler 拒绝策略(队列满时触发) 根据业务敏感度选择

注:N = Runtime.getRuntime().availableProcessors()(CPU 核数)

线程数配置的经验公式:

  • CPU 密集型(加密、计算、图像处理):线程数 = CPU 核数 + 1
  • IO 密集型(网络请求、数据库查询、文件读写):线程数 = CPU 核数 × 2 或更高
  • 混合型:根据 IO 等待时间占比调整,公式:线程数 = N × (1 + 等待时间/计算时间)

四、四种拒绝策略对比

// 1️⃣ AbortPolicy(默认)—— 抛出 RejectedExecutionException
// 适合:关键业务,宁可失败也不能静默丢弃
new ThreadPoolExecutor.AbortPolicy();

// 2️⃣ CallerRunsPolicy —— 由提交任务的线程自己执行
// 适合:削峰填谷,降低提交速度,生产环境推荐
new ThreadPoolExecutor.CallerRunsPolicy();

// 3️⃣ DiscardPolicy —— 静默丢弃,不抛异常
// 适合:非核心业务,日志采集等可丢弃场景
new ThreadPoolExecutor.DiscardPolicy();

// 4️⃣ DiscardOldestPolicy —— 丢弃队列中最老的任务,再尝试提交
// 适合:实时性要求高的场景,老任务可以丢弃
new ThreadPoolExecutor.DiscardOldestPolicy();
拒绝策略行为适用场景
AbortPolicy 抛异常,快速失败 关键业务,需要感知失败
CallerRunsPolicy 调用者线程执行 削峰填谷,生产环境推荐
DiscardPolicy 静默丢弃 非核心业务,可容忍丢失
DiscardOldestPolicy 丢弃最老任务 实时性要求高

五、方式 3:ScheduledThreadPoolExecutor(定时任务专用)

适合需要 延迟执行 或 周期性执行 的场景:

// 创建定时任务线程池
ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(2);

// ① 延迟执行:3 秒后执行一次
scheduler.schedule(() -> {
    System.out.println(“3 秒后执行一次“);
}, 3, TimeUnit.SECONDS);

// ② 固定频率执行(不管上次是否完成)
scheduler.scheduleAtFixedRate(() -> {
    System.out.println(“每 5 秒执行一次“);
}, 1, 5, TimeUnit.SECONDS);

// ③ 固定延迟执行(上次执行完后再等待)
scheduler.scheduleWithFixedDelay(() -> {
    System.out.println(“执行完后等 5 秒再执行“);
}, 1, 5, TimeUnit.SECONDS);

六、方式 4:ForkJoinPool(分治计算专用)

JDK 7 引入,专门用于 递归分解 的计算密集型任务,采用 工作窃取 算法:

// 示例:大数组求和
ForkJoinPool pool = new ForkJoinPool(4);
long result = pool.invoke(new SumTask(array, 0, array.length));

适用场景:大数组求和、并行排序、树的遍历、矩阵运算等可以递归分解的计算密集型任务。

注意:JDK 8 的 parallelStream() 底层默认使用 ForkJoinPool.commonPool()

posted @ 2026-03-10 10:07  网络大法师  阅读(11)  评论(0)    收藏  举报