编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • 性能优化实践

  • 程序编程原理

    • README
    • 序卷方法论

    • 数据的本质

    • 运行时模型

    • 并发的设计

      • README
      • 1.线程前世今生探索
      • 2.并发上下文切换原理
      • 3.线程通信设计思想
      • 4.线程异常设计原理
      • 5.多线程并发经典案例
      • 6.并发Bug源头由来
      • 7.并发编程设计思想
      • 8.并发编程安全设计
      • 9.锁核心设计和思想
      • 10.理解CAS设计由来
      • 11.异步和同步的设计
      • 12.单线程模型的思想
      • 13.协程核心设计思想
      • 14.Actor与CSP并发模型
      • 15.线程池的设计思想
        • 01.池化思想探索
          • 1.1 什么是池化思想
          • 1.2 池化的优势
          • 1.3 池化具体体现
          • 1.4 一般池的场景
          • 1.5 线程池核心设计
        • 02.线程池设计框架
          • 2.1 核心原理和架构
          • 2.2 线程复用思想
          • 2.3 要有任务队列
          • 2.4 合理资源管理
          • 2.5 任务调度思想
        • 03.线程池工作流程
          • 3.1 提交线程任务
          • 3.2 判断核心线程数
          • 3.3 判断任务队列
          • 3.4 判断最大线程数
          • 3.5 开始执行任务
          • 3.6 线程回收处理
          • 3.7 执行流程图
        • 04.线程复用设计
          • 4.1 线程复用原理
          • 4.2 线程生命周期管理
          • 4.3 线程复用实现示例
          • 4.4 核心线程设计
        • 05.线程池管理设计
          • 5.1 线程池状态管理
          • 5.2 线程池参数配置
          • 5.3 线程池管理策略
        • 06.任务队列设计
          • 6.1 队列类型对比
          • 6.2 队列类型
          • 6.3 队列选择策略
        • 07.任务调度与设计
          • 7.1 任务提交过程
          • 7.2 任务调度策略
          • 7.3 拒绝策略
        • 08.添加线程任务
          • 8.1 任务提交接口
          • 8.2 任务封装
          • 8.3 任务提交示例
        • 09.线程回收机制
          • 9.1 线程回收时机
          • 9.2 线程回收实现
          • 9.3 线程回收示例
          • 9.4 线程池监控设计
          • 9.5 线程池设计总结
        • 10.跨语言设计哲学对比
          • 10.1 Java:7 参数设计的哲学
          • 10.2 Go:为什么不需要线程池?
          • 10.3 Python:GIL下的两种线程池
          • 10.4 C#/.NET:Task+ThreadPool分层
          • 10.5 跨语言设计哲学表
        • 11.经典陕阱与事故复盘
          • 11.1 事故①|newFixedThreadPool导致OOM
          • 11.2 事故②|共用线程池导致死锁
          • 11.3 事故③|任务异常被吞掉
          • 11.4 事故④|ThreadLocal不清理泄露
          • 11.5 事故⑤|CallerRunsPolicy反锁主线程
        • 12.一句话总结
        • 📎 延伸阅读
      • 16.线程池设计核心原理
      • 17.线程池使用技巧
      • 18.结构化并发设计思想
    • 内存的真相

    • 交互和系统

  • 稳定性与可靠性

  • 工程化与运维

  • 方案设计思想

  • 专栏
  • 程序编程原理
  • 并发的设计
杨充
2025-10-27
目录

15.线程池的设计思想

# 25.线程池的设计思想

📍 本篇位置:第 3 卷 · 并发之道 · 第 15 篇(落地三部曲之一) 🎯 核心矛盾:线程的创建开销 vs 任务的短平快 —— 用"池"解决"一次性"的浪费 🧭 设计灵魂:线程池 = 对象池 + 任务队列 + 调度策略;它是"生产者-消费者"模式在并发工程里最重要的一次具象化 🌐 跨语言覆盖:Java(ThreadPoolExecutor 七参数) · C++(手写 + std::thread + queue) · Python(concurrent.futures) · Go(goroutine 自带无需显式池) · C#(ThreadPool + Task) 🔗 延伸阅读:← 19.并发上下文切换原理 · → 26.线程池使用技巧 · → 27.线程池设计核心原理

flowchart TB
    A[任务洪峰到达] --> B[线程池三件套]
    B --> C1[核心线程<br/>常驻]
    B --> C2[阻塞队列<br/>缓冲]
    B --> C3[临时线程<br/>削峰]
    C1 & C2 & C3 --> D{超出容量}
    D -->|是| E[拒绝策略<br/>抛弃/回退/调用方执行]
    D -->|否| F[任务被消费]
    style B fill:#fff3cd
1
2
3
4
5
6
7
8
9

# 目录介绍

  • 01.池化思想探索
    • 1.1 什么是池化思想
    • 1.2 池化的优势
    • 1.3 池化具体体现
    • 1.4 一般池的场景
    • 1.5 线程池核心设计
  • 02.线程池设计框架
    • 2.1 核心原理和架构
    • 2.2 线程复用思想
    • 2.3 要有任务队列
    • 2.4 合理资源管理
    • 2.5 任务调度思想
    • 2.6 线程池状态图
  • 03.线程池工作流程
    • 3.1 提交线程任务
    • 3.2 判断核心线程数
    • 3.3 判断任务队列
    • 3.4 判断最大线程数
    • 3.5 开始执行任务
    • 3.6 线程回收处理
    • 3.7 执行流程图
  • 04.线程复用设计
    • 4.1 线程复用原理
    • 4.2 线程生命周期管理
    • 4.3 线程复用实现示例
    • 4.4 核心线程设计
  • 05.线程池管理设计
    • 5.1 线程池状态管理
    • 5.2 线程池参数配置
    • 5.3 线程池管理策略
  • 06.任务队列设计
    • 6.1 队列类型对比
    • 6.2 队列类型
    • 6.3 队列选择策略
  • 07.任务调度与设计
    • 7.1 任务提交过程
    • 7.2 任务调度策略
    • 7.3 拒绝策略
  • 08.添加线程任务
    • 8.1 任务提交接口
    • 8.2 任务封装
    • 8.3 任务提交示例
  • 09.线程回收机制
    • 9.1 线程回收时机
    • 9.2 线程回收实现
    • 9.3 线程回收示例

# 01.池化思想探索

# 1.1 什么是池化思想

“池化”不是个现代词。人类发明“预备几个、同一名额、谁需谁拿”这件事已经上千年。

  仓库:不是谁买东西才进货、而是预备会出售的存量
  出租车队:不是谁叫车才叫司机上班、而是预备一批可快速响应的司机
  医院育鬼集:不是谁需要才发起、而是预存一批随时取用
1
2
3

软件工程哲学上的“池化”是同一个思想的代码体现:

  预先准备一批可重复使用的资源、需要时从池里拿、用完不销毁、放回池里。
1

这个思想可以很多资源都可以被“池化”:

资源 序列化代价 被池化后的收益
数据库连接 TCP 三次握手 + SSL、几十 ms HikariCP、Druid
线程 内核在定 + 栈划拨 + TLS、~1ms + 几 MB ThreadPoolExecutor
HTTP 连接 TCP + TLS 握手、几百 ms OkHttp ConnectionPool
对象实例 new + GC Netty ByteBuf Recycler、Apache Commons Pool
内存页 页错 + 内核分配 内存池(jemalloc、tcmalloc)

池化本质上是一种抵抗“高频起调”的谜语——只要某种资源 "创建贵、五粉五粉、使用频”、就有默认被池化的价值。

# 1.2 池化的优势

你可能听过“减少创建开销”这句话几百遍。但创建开销到底多高?按价取证你会发现这个优势强到震惊。

  创建一个 OS 线程的底层代价:
  - clone 系统调用    ~ 50,000 个 CPU 周期
  - 实体存初始化       ~ 1MB 虚存 + 页表
  - TLS 清零              ~ KB 级别
  - 加入就绪队列        ~ 内核锁 + 调度器发烧

  以一台 3GHz 机器上、单个线程创建费时约 100~500 μs
  一秒内创建 10000 个线程:光创建本身吃 100% CPU 不够补
1
2
3
4
5
6
7
8

所以池化的四大优势不是“老生常谈”、是被“数字”逼着走这条路。

优势①|降低资源创建/销毁开销 频繁创建和销毁线程(或其它资源)会导致系统性能下降,池化通过复用线程降低开销。

优势②|提高响应速度 任务到达时,池中已有线程可立即执行,无需等待线程创建。这在高并发场景里是决定性的。

优势③|可控制资源使用 通过限制池中资源数量,防止资源过度使用导致系统崩溃。这是被上万次服务雪崩逼出来的反身——不限制资源 = 不可预测。

优势④|统一管理 便于对资源进行监控、调优和统计。这是生产环境里池化的“隐藏价值”——你能看到 activeCount、queueSize、能动态调。

# 1.3 池化具体体现

线程池通过维护一定数量的线程,避免为每个任务都创建新线程,从而减少系统开销。

# 1.4 一般池的场景

对这种频繁需要创建和销毁的对象保存在一个对象池中。每次用到该对象时,就取对象池空闲的对象,并对它进行初始化操作,从而提高框架的性能。这种对象池的设计核心代码如下所示

//采用一般意义上池化资源的设计方法
public class SimplePool<T> implements Pool<T> {
    // 获取空闲线程
    T acquire() {
    }
    // 释放线程
    void release(T t){
    }
}
class Test{
    public void test() {
        //期望的使用
        SimplePool<Thread> pool = new SimplePool<>();
        //使用
        Thread t1 = pool.acquire();
        //传入Runnable对象
        t1.execute(()->{
          //具体业务逻辑
        });
        //释放
        pool.release(t1);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

为什么线程池没有采用一般意义上池化资源的设计方法呢?

如果线程池采用一般意义上池化资源的设计方法,你可以来思考一下,假设我们获取到一个空闲线程 T1,然后该如何使用 T1 呢?

你期望的可能是这样:通过调用 T1 的 execute() 方法,传入一个 Runnable 对象来执行具体业务逻辑,就像通过构造函数 Thread(Runnable target) 创建线程一样。

可惜的是,你翻遍 Thread 对象的所有方法,都不存在类似 execute(Runnable target) 这样的公共方法。

# 1.5 线程池核心设计

那线程池该如何设计呢?目前业界线程池的设计,普遍采用的都是生产者-消费者模式。线程池的使用方是生产者,线程池本身是消费者。

flowchart LR
    subgraph PRODUCERS["生产者、Producers"]
        P1[业务线程 1]
        P2[业务线程 2]
        P3[业务线程 3]
    end
    subgraph QUEUE["任务队列、Buffer"]
        Q[BlockingQueue]
    end
    subgraph CONSUMERS["消费者、Workers"]
        W1[Worker 1]
        W2[Worker 2]
        Wn[Worker N]
    end
    P1 -->|submit| Q
    P2 -->|submit| Q
    P3 -->|submit| Q
    Q -->|take| W1
    Q -->|take| W2
    Q -->|take| Wn
    style QUEUE fill:#fff3cd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

在线程池中,线程池本身可以被视为生产者,它负责创建和管理线程资源。任务(或工作单元)可以被视为生产者生产的产品,线程池中的线程则是消费者,负责消费这些任务并执行相应的操作。

为什么生产者-消费者能作为线程池的骨架?

这是个被多个限制源头联动逼出来的选择:

限制①:生产者与消费者速度不一致、必须有缓冲
  → 为什么要 BlockingQueue
限制②:资源有限、不能无限堆积
  → 为什么队列需要有界 + 拒绝策略
限制③:调度必须是线程安全的、多个 Worker 不能同取一任务
  → 为什么队列是 BlockingQueue 而不是普通 Queue
限制④:Worker 要能安全退出、也要能被中断唤醒
  → 为什么需要 状态机 + Lock + Condition
1
2
3
4
5
6
7
8

这 4 个限制一叠加、生产者-消费者模式就是唯一能同时解决这 4 件事的设计。不是设计者的偏好、是数学上的唯一选项。

# 02.线程池设计框架

线程池的抽象架构模型,这种分层架构实现了关注点分离,每层负责特定的职责,通过标准接口协作。 一个完整的线程池包含以下核心抽象层:

┌─────────────────────────────────────────────────────────────┐
│                   应用层接口 (Application Layer)              │
├─────────────────────────────────────────────────────────────┤
│  submit() ─────── execute() ─────── invoke() ─────── schedule() │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────┐
│                 任务调度层 (Scheduling Layer)                 │
├─────────────────────────────────────────────────────────────┤
│  任务队列 ─────── 拒绝策略 ─────── 饱和策略 ─────── 优先级调度      │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────┐
│                 线程管理层 (Thread Management Layer)           │
├─────────────────────────────────────────────────────────────┤
│  线程创建 ─────── 线程回收 ─────── 空闲管理 ─────── 异常处理        │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────┐
│                 资源监控层 (Monitoring Layer)                 │
├─────────────────────────────────────────────────────────────┤
│  指标收集 ─────── 状态追踪 ─────── 性能分析 ─────── 动态调优        │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 2.1 核心原理和架构

线程池是一种管理和复用线程的机制,旨在提高多线程程序的性能和资源利用率。其核心原理是通过预先创建一定数量的线程,并将任务分配到这些线程中执行,从而避免频繁创建和销毁线程的开销。

graph TB
    subgraph "客户端层"
        A[任务提交者] --> B[ThreadPoolExecutor]
    end
    
    subgraph "线程池核心层"
        B --> C[任务队列 BlockingQueue]
        B --> D[线程管理器]
        B --> E[拒绝策略处理器]
        
        D --> F[核心线程池]
        D --> G[扩展线程池]
        
        F --> H[Worker Thread 1]
        F --> I[Worker Thread 2]
        F --> J[Worker Thread N]
        
        G --> K[临时线程 1]
        G --> L[临时线程 2]
    end
    
    subgraph "任务执行层"
        H --> M[任务执行]
        I --> M
        J --> M
        K --> M
        L --> M
        
        M --> N[任务完成回调]
        M --> O[异常处理]
    end
    
    subgraph "监控管理层"
        P[线程池监控] --> B
        Q[性能统计] --> B
        R[健康检查] --> B
    end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

# 2.2 线程复用思想

为什么“线程复用”是线程池的灵魂?这要从“线程本质上是昂贵的”说起。

问题推导:频繁创建和销毁线程会消耗大量系统资源,降低程序性能。不仅是 “创建贵”、还是“销毁也贵”——销毁需要 等 worker 跳出 run、释放栈空间、报 GC 、可能还需要等其它线程唤醒。

解决方案:线程池预先创建一组线程,任务到来时直接复用这些线程,避免重复创建和销毁。

这里有一个反直觉的点:你以为“同一个线程跳多件事”是个艰巨设计,实际上这是最高效的设计。原因在于上下文切换不只是保存寄存器、还会导致 L1/L2/L3 缓存全部失效。一个线程跑在同一个核上、能享受缓存心热、这是创建新线程远远达不到的优势。

线程复用的实现三步:

步骤 1|预先创建线程 在初始化时创建一组线程、并使其处于等待状态。

// JDK 原生实现
private boolean addWorker(Runnable firstTask, boolean core) {
    // 创建 Worker、Worker 本身是一个 Runnable + 一个 Thread
    Worker w = new Worker(firstTask);
    final Thread t = w.thread;
    t.start();   // 从这一刻起、线程就跑在 Worker.run() 里了
}
1
2
3
4
5
6
7

步骤 2|任务分配 当任务到来时、将任务分配给空闲线程执行。实际上不是“分配”、是“任务入队、Worker 主动拾”。

步骤 3|线程回收 线程执行完任务后、不销毁、而是重新进入空闲状态、等待下一个任务。心脲的是:这个“循环”是在 worker 自己的 run() 里实现的、不需要外部控制:

final void runWorker(Worker w) {
    while (task != null || (task = getTask()) != null) {
        task.run();
        // 跑完取下一个任务、一直循环
    }
}
1
2
3
4
5
6

这是“任务其实不是被推到线程上、是被线程拍走的”——这个反转是理解线程池的关键。

# 2.3 要有任务队列

问题推导:当任务数量超过线程池的处理能力时,需要一种机制来缓冲任务。不然业务会被丢、或者线程会被炸。

解决方案:线程池使用任务队列(如阻塞队列)来存储待执行的任务,确保任务不会丢失。

为什么一定要是“阻塞队列”?

这是个面试常问点、但讲清楚的人不多。换个“普通队列”会怎么样?

// 如果用普通队列、Worker 取任务需要这样写
while (true) {
    Runnable task = queue.poll();
    if (task != null) { task.run(); continue; }
    Thread.sleep(10);   // ⚠️ 只能忙轮询、或 sleep 一下
}
1
2
3
4
5
6

这不是设计陕阱、是居礻性问题。“sleep 多久”是个永远错误的选择:sleep 夭短 = CPU 被轮询烧炽;sleep 夭长 = 任务响应迟延。

阻塞队列用 Lock + Condition 优雅解决了这个问题:

  队空   → Worker 调 take()、被挂起 、不耗 CPU
  任务来 → 生产者 offer、Condition.signal 唤醒 Worker
  队满   → 生产者 put、被挂起 (有界队列)、反压到上游
1
2
3

这不是“一个队列”、是“一个同步原语 + 一个缓冲区”的复合实体。生产者-消费者模式的哲学在这里被一句话实体化。

# 2.4 合理资源管理

问题推导:线程数量过多会消耗大量系统资源,甚至导致系统崩溃。这个“多”到什么程度?

JVM 默认 -Xss = 1MB、一个线程需几 MB、主要是栈
Linux 默认 ulimit -n 限制 fd 数、限制了线程最大数量
Linux 默认 pid_max = 32768、超过不能创建新线程

  线程超过 1万、多数 JVM 会推入稳定堆使用不可控区
  线程超过 5万、Linux 默认参数下会招不住、调度器变慢
1
2
3
4
5
6

解决方案:线程池通过限制线程数量(核心线程数、最大线程数)和任务队列大小,合理管理系统资源。

三道闸门限制资源:

第一道:corePoolSize        → 常驻、不会被回收
第二道:workQueue 容量      → 生产-消费不一致时的缓冲、避免丢任务
第三道:maximumPoolSize     → 临时、项临时雷峰、峰过后被回收
超过 → RejectedExecutionHandler 拒绝、反压到业务层
1
2
3
4

这个设计的高明处:使用 三个闸门 而不是 一个闸门 来限制资源——这让线程池能**同时适应“平稳负载”和“突发雷峰”**两种场景。平稳期只需要 corePoolSize 个线程;雷峰期会临时报动 maximumPoolSize 个线程、峰过后释放。弹性限流的本质。

# 2.5 任务调度思想

问题:如何高效地将任务分配给线程执行。

解决方案:线程池根据任务队列的状态和线程的可用性,动态调度任务。

# 03.线程池工作流程

线程池的工作流程一般是这样的:

  1. 初始化线程池:创建一组线程。
  2. 提交任务:当调用 execute() 或 submit() 方法提交任务时,线程池会检查当前线程数是否小于核心线程数。
  3. 任务入队:如果线程数已达到核心线程数,则将任务放入任务队列中等待执行。
  4. 拒绝任务:如果任务队列已满且线程数已达到最大线程数,则根据拒绝策略处理新提交的任务。
  5. 任务执行完:线程执行完任务后,返回空闲状态,等待下一个任务。
  6. 线程回收:当线程池关闭时,销毁所有线程。

任务提交与执行时序图

sequenceDiagram
    participant Client as 客户端
    participant TPE as ThreadPoolExecutor
    participant Queue as 任务队列
    participant Worker as 工作线程
    participant Task as 任务
    
    Note over Client,Task: 任务提交阶段
    Client->>TPE: execute(task)
    TPE->>TPE: 检查线程池状态
    
    alt 核心线程数未满
        TPE->>Worker: 创建新的核心线程
        Worker->>Task: 直接执行任务
    else 核心线程数已满
        TPE->>Queue: 将任务加入队列
        alt 队列未满
            Queue-->>TPE: 任务入队成功
            Note over Worker: 空闲线程从队列获取任务
            Worker->>Queue: getTask()
            Queue->>Worker: 返回任务
            Worker->>Task: 执行任务
        else 队列已满
            alt 线程数 < 最大线程数
                TPE->>Worker: 创建临时线程
                Worker->>Task: 直接执行任务
            else 线程数 = 最大线程数
                TPE->>TPE: 执行拒绝策略
                TPE-->>Client: 抛出异常或其他处理
            end
        end
    end
    
    Note over Client,Task: 任务执行阶段
    Worker->>Task: 执行run()方法
    Task-->>Worker: 任务完成
    Worker->>TPE: 更新统计信息
    
    Note over Client,Task: 线程回收阶段
    alt 线程空闲时间超过keepAliveTime
        Worker->>TPE: 请求销毁线程
        TPE->>Worker: 销毁线程
    else 继续等待新任务
        Worker->>Queue: getTask()
    end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

# 3.1 提交线程任务

提交任务看似只是一句 executor.submit()、实际上背后有两个不同 API 设计:

// API 一:execute - 什么都不返回、错误会被 UncaughtExceptionHandler 报
executor.execute(() -> doWork());

// API 二:submit - 返回 Future、错误被包装在 Future.get() 里
Future<?> f = executor.submit(() -> doWork());
f.get();   // 调用才能取得错误
1
2
3
4
5
6

两个 API 背后最后调用的都是同一个 execute(Runnable),submit 有几个额外封装:

submit(Runnable)        → 包装 RunnableFuture → execute()
submit(Callable<T>)     → 包装 RunnableFuture<T> → execute()
1
2

execute 为什么设为 void?这是设计上反复考虑后的选择。execute 是“开火即忘”语义,不需要报任何状态。你需要状态就用 submit 。这与 Linux fork 的 “不返回”设计有极高哲学相似。

# 3.2 判断核心线程数

if (workerCountOf(c) &lt; corePoolSize) {
    if (addWorker(command, true)) return;
}
1
2
3

为什么是“小于”而不是“小于等于”? 这个细节让众多人绑动反。

if (corePool < N)         创建     // ----- 这个纯是“严格小于”
if (corePool <= N)        创建     // 错误什说、会创建 N+1 个线程
1
2

该逻辑器仅在当前线程数 严格小于 core 时创建。然后存在 CAS 补偿:两个线程同时提交且同时看到 “少于 core”、但二者只能一个能肩负创建。其他线程 CAS 失败、会进入下一个判断。

# 3.3 判断任务队列

if (isRunning(c) &amp;&amp; workQueue.offer(command)) {
    // 二次检查、避免事件交错
    if (! isRunning(recheck) &amp;&amp; remove(command))
        reject(command);
}
1
2
3
4
5

为什么需要二次检查? 这是多线程设计中经典的 DCL(Double-Check Locking)。场景:

T1 判断“运行中”、准备 offer
T2 同时调 shutdown()
T1 offer 成功、但线程池已不运行了
二次检查:发现 不运行 → 把任务从队列拽出、走拒绝策略
1
2
3
4

这个细节体现了 ThreadPoolExecutor 在并发设计上的严谨。

# 3.4 判断最大线程数

这是“雷峰响应”的最后一道闸门:队满则试图报动临时线程。

if (workerCountOf(c) &lt; maximumPoolSize) {
    if (addWorker(command, false)) return;
}
reject(command);   // 最后拒绝
1
2
3
4

临时线程与核心线程代码层完全一样,区别仅在于创建时传入 core=false 、这个参数仅是插入判断“超过 corePoolSize 后判定走 SCHED 还是走 max”。这个设计极为精巧——不需要为“核心”和“临时”创建两种 Worker 类,只需要调用时拼接一个标签。

# 3.5 开始执行任务

任务执行不是“调 task.run()”这么简单,Worker 的 runWorker() 干了四件事:

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    while (task != null || (task = getTask()) != null) {
        w.lock();
        try {
            beforeExecute(wt, task);   // ① 抩子、子类可重写、打点
            try { task.run(); }         // ② 跑任务、唯一的业务逻辑
            catch (Throwable x) { thrown = x; throw x; }
            finally { afterExecute(task, thrown); }   // ③ 抹尾、走费、释放
        } finally {
            w.unlock();
            completedTasks++;          // ④ 计数
            task = null;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

为什么设计成 beforeExecute / afterExecute? 这是为“可观测性”量体预留的钩子。子类可重写:

  • 抹除考出任务运行时间(同机上报)
  • 报发 traceId 、为全链路追踪推进上下文
  • 护反 ThreadLocal 清理、避免股型泄露

# 3.6 线程回收处理

线程回收什不是 “销毁线程”、是 “让 worker 从 getTask() 返回 null、自然退出 run()”。

private Runnable getTask() {
    boolean timed = wc > corePoolSize || allowCoreThreadTimeOut;
    Runnable r = timed ?
        workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :   // 有会返回 null
        workQueue.take();                                       // 永远不会返回 null
    if (r != null) return r;
    // null 表示“超时了、你被回收了”
    return null;
}
1
2
3
4
5
6
7
8
9

这里有个面试震援题:能不能让核心线程也被回收?

能。调用 executor.allowCoreThreadTimeOut(true)、所有 worker 都会参与超时回收。低负载时能进一步节省资源、但下一个任务来时会付出创建代价。这是“零资源资源”与“快响应”的权衡。

# 3.7 执行流程图

flowchart TD
    A[任务提交] --> B{线程池是否运行中?}
    B -->|否| C[执行拒绝策略]
    B -->|是| D{当前线程数 < 核心线程数?}
    
    D -->|是| E[创建新核心线程]
    E --> F[线程直接执行任务]
    
    D -->|否| G{任务队列是否已满?}
    G -->|否| H[任务加入队列]
    H --> I[空闲线程获取任务]
    I --> J[执行任务]
    
    G -->|是| K{当前线程数 < 最大线程数?}
    K -->|是| L[创建临时线程]
    L --> M[临时线程执行任务]
    
    K -->|否| N[执行拒绝策略]
    
    F --> O[任务执行完成]
    J --> O
    M --> O
    
    O --> P{线程是否为临时线程?}
    P -->|是| Q{空闲时间 > keepAliveTime?}
    Q -->|是| R[销毁线程]
    Q -->|否| S[继续等待任务]
    
    P -->|否| T{允许核心线程超时?}
    T -->|是| Q
    T -->|否| S
    
    S --> U[从队列获取下一个任务]
    U --> V{获取到任务?}
    V -->|是| J
    V -->|否| W{线程池是否关闭?}
    W -->|是| R
    W -->|否| S
    
    C --> X[结束]
    N --> X
    R --> X
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# 04.线程复用设计

# 4.1 线程复用原理

线程复用的核心是让线程在执行完一个任务后不退出,而是继续执行下一个任务。这通过一个循环结构实现,线程不断从任务队列中获取任务并执行。

线程复用的核心是将线程创建成本分摊到多个任务执行周期。其理论基础是线程上下文切换成本远低于进程创建成本。

# 4.2 线程生命周期管理

创建:线程池初始化时创建一定数量的线程(核心线程),这些线程处于等待任务状态。 运行:线程从任务队列中获取任务并执行。 等待:当任务队列为空时,线程进入等待状态(通过条件变量或锁机制)。 销毁:当线程池决定减少线程数量时,某些线程会被终止。线程池可以设置线程的空闲时间,超过该时间没有任务执行则销毁线程(非核心线程)。

stateDiagram-v2
    [*] --> NEW: 创建线程
    NEW --> RUNNABLE: start()
    RUNNABLE --> BLOCKED: 等待锁
    RUNNABLE --> WAITING: wait()/join()
    RUNNABLE --> TIMED_WAITING: sleep()/wait(timeout)
    BLOCKED --> RUNNABLE: 获得锁
    WAITING --> RUNNABLE: notify()/notifyAll()
    TIMED_WAITING --> RUNNABLE: 超时/notify()
    RUNNABLE --> TERMINATED: 任务完成/异常
    TERMINATED --> [*]
1
2
3
4
5
6
7
8
9
10
11

一个可复用的线程包含完整的状态转换:

创建(CREATED) → 就绪(READY) ↔ 运行(RUNNING) ↔ 等待(WAITING) 
               ↘ 终止(TERMINATED)
1
2

复用线程的关键是避免进入终止状态,在任务完成后回到就绪状态等待新任务。

# 4.3 线程复用实现示例

以下是一个简单的线程复用示例(C++伪代码):

class WorkerThread {
private:
    ThreadPool* pool;
    bool running;
public:
    void run() {
        while (running) {
            Task task = pool->getTask(); // 从任务队列获取任务,如果队列为空则阻塞
            if (task) {
                task.execute();
            }
        }
    }
    void stop() { running = false; }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 4.4 核心线程设计

疑惑:核心线程和非核心线程在代码层面有什么区别?是不是有两种不同的Thread对象?

答疑:核心线程和非核心线程在代码层面没有任何区别!它们是同一种Worker对象。"核心"与"非核心"仅仅是一个计数上的概念——当线程池决定回收空闲线程时,会判断当前线程数是否超过corePoolSize,超过的部分在空闲keepAliveTime后被回收。

// getTask()中的关键逻辑(简化版)
private Runnable getTask() {
    for (;;) {
        int wc = workerCountOf(ctl.get());
        
        // 关键判断:当前线程数 > 核心线程数?
        boolean timed = wc > corePoolSize;
        
        // timed=true: 用poll(keepAliveTime),超时返回null → 线程退出
        // timed=false: 用take(),永远等待 → 核心线程不退出
        Runnable r = timed ? 
            workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
            workQueue.take();
        
        if (r != null) return r;
        // r==null说明超时了 → 这个线程被当作"非核心线程"回收
        return null;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

本质:哪个线程是"核心"哪个是"非核心",取决于被回收那一刻的线程总数,而不是线程创建时的身份。这是一个非常优雅的设计——不需要给线程打标签,只需要在回收时做数量判断。

核心线程也可以被回收,通过设置allowCoreThreadTimeOut(true),所有线程在空闲超时后都会退出。这在低负载时能进一步节省资源。

# 05.线程池管理设计

# 5.1 线程池状态管理

线程池状态图

stateDiagram-v2
    [*] --> RUNNING: 创建线程池
    RUNNING --> SHUTDOWN: 调用shutdown()
    RUNNING --> STOP: 调用shutdownNow()
    SHUTDOWN --> TIDYING: 所有任务完成
    STOP --> TIDYING: 所有线程停止
    TIDYING --> TERMINATED: 执行terminated()
    TERMINATED --> [*]
    
    note right of RUNNING
        接受新任务
        处理队列中的任务
    end note
    
    note right of SHUTDOWN
        不接受新任务
        继续处理队列中的任务
    end note
    
    note right of STOP
        不接受新任务
        不处理队列中的任务
        中断正在执行的任务
    end note
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

线程池通常有以下状态:

状态 值 描述
RUNNING -1 接受新任务并处理队列中的任务,正常处理任务
SHUTDOWN 0 不接受新任务,但会处理队列中的任务
STOP 1 不接受新任务,不处理队列中的任务,并中断正在执行的任务
TIDYING 2 所有任务已终止,工作线程数为0
TERMINATED 3 terminated()方法已完成

# 5.2 线程池参数配置

核心线程数(corePoolSize):线程池维护的最小线程数量,即使它们处于空闲状态。 最大线程数(maximumPoolSize):线程池允许的最大线程数量。 任务队列(workQueue):用于保存等待执行的任务的阻塞队列。 线程工厂(threadFactory):用于创建新线程。 拒绝策略(rejectedExecutionHandler):当任务队列已满且线程数达到最大值时,如何处理新任务。

public class ThreadPoolConfig {
    // 核心线程数:始终保持活跃的线程数量
    private int corePoolSize;
    
    // 最大线程数:线程池允许的最大线程数量
    private int maximumPoolSize;
    
    // 线程空闲时间:非核心线程的最大空闲时间
    private long keepAliveTime;
    
    // 时间单位
    private TimeUnit unit;
    
    // 任务队列:存储待执行任务的队列
    private BlockingQueue<Runnable> workQueue;
    
    // 线程工厂:用于创建新线程
    private ThreadFactory threadFactory;
    
    // 拒绝策略:当任务无法执行时的处理策略
    private RejectedExecutionHandler handler;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
应用场景 核心线程数 最大线程数 队列类型 队列大小
CPU密集型 CPU核心数 CPU核心数+1 LinkedBlockingQueue 无界
IO密集型 2*CPU核心数 4*CPU核心数 ArrayBlockingQueue 1000-5000
混合型 CPU核心数+1 2*CPU核心数 LinkedBlockingQueue 2000-10000
高并发短任务 10-50 100-200 SynchronousQueue 0

# 5.3 线程池管理策略

线程创建策略:当任务数小于核心线程数时,创建新线程(即使有空闲线程);当任务数大于核心线程数且任务队列已满时,创建新线程直到达到最大线程数。 线程销毁策略:当线程空闲时间超过设定的keepAliveTime,且当前线程数大于核心线程数时,销毁该线程。

# 06.任务队列设计

# 6.1 队列类型对比

任务队列用于保存等待执行的任务。当线程池中的线程都在忙碌时,新任务会被放入队列中等待。

graph LR
    A[BlockingQueue接口] --> B[ArrayBlockingQueue]
    A --> C[LinkedBlockingQueue]
    A --> D[SynchronousQueue]
    A --> E[PriorityBlockingQueue]
    A --> F[DelayQueue]
    
    B --> B1[有界数组队列<br/>固定容量<br/>FIFO顺序]
    C --> C1[无界链表队列<br/>可选容量<br/>FIFO顺序]
    D --> D1[同步队列<br/>容量为0<br/>直接传递]
    E --> E1[优先级队列<br/>无界队列<br/>优先级顺序]
    F --> F1[延迟队列<br/>无界队列<br/>延迟执行]
1
2
3
4
5
6
7
8
9
10
11
12

# 6.2 队列类型

队列选型其实是个“三选一”谜题:无界 / 有界 / 同步移交。三者面对同一个问题提供三种完全不同的哲学。

选择①|无界队列:“永远不拒”

new LinkedBlockingQueue<Runnable>();   // 默认 Integer.MAX_VALUE
1

哲学:业务”任务不能丢“最重要、内存需多少给多少。阅颈陕阱:生产-消费一旦不均衡,队列会吃光堆内存。 适用场景:生产速率 < 消费速率、以及任务必须不能丢。

选择②|有界队列:“足了拒绝”

new ArrayBlockingQueue<>(1000);
new LinkedBlockingQueue<>(1000);
1
2

哲学:”服务稳定“最重要、任务丢了也宁可不能 OOM。与拒绝策略配合使用、是生产环境默认选择。 该如何选容量?一般是 “core 线程数 × 任务平均耗时 / 最大允许响应时间”。例:Core=20, 任务 50ms, P99=2s → 队列 ≈ 800。

选择③|同步移交队列:“永远不会被入队”

new SynchronousQueue<Runnable>();
1

这个设计是反直觉的:容量 = 0、offer 决不会成功(除非有另一个线程在 take)。这意味着任务提交后立即开 max、拒绝。阅原代:最优先物反拒接受原、快热锁、低反压 → CachedThreadPool。

选择④|优先级队列:PriorityBlockingQueue、用于带优先级的任务调度。反设计:优先级队列是贴底的”不公平“、低优先级任务可能被饱餁。需同时选 “优先级起升 / 老化策略”。

选择⑤|延迟队列:DelayQueue、用于定时任务 ScheduledThreadPoolExecutor 的逆块。任务入队时带 “定期起跟点 + 执行间隔”信息、到点才被 take 。

# 6.3 队列选择策略

无界队列适用于任务提交速度不快于线程处理速度的场景,避免任务被拒绝。

有界队列可以防止资源耗尽,但需要设置合适的队列大小和拒绝策略。

队列类型 适用场景 优点 缺点
ArrayBlockingQueue 有界缓冲,防止内存溢出 内存占用可控 容量固定,可能阻塞
LinkedBlockingQueue 高吞吐量场景 吞吐量高 可能导致内存溢出
SynchronousQueue 直接传递,快速响应 响应速度快 需要足够多的线程
PriorityBlockingQueue 任务有优先级要求 支持优先级 排序开销

# 07.任务调度与设计

# 7.1 任务提交过程

任务提交的决策顺序是一个三点金字塔,随着资源压力逐步升高。

优先级 1:使用核心线程      → "资源足够、护礼处理"
优先级 2:压入任务队列      → "护资源、缓冲一下"
优先级 3:抨动临时线程      → "极限雷峰、临时抨”
优先级 4:拒绝策略            → "到黑了、送丢老智。谁退丢、怎么退丢"
1
2
3
4

为什么不“队列与临时线程同时动作”?

这是 Java 设计者的意识选择、与其它语言不同:

Java:    core 满 → 队列 → 队满才抨动 max
.NET:    core 满 → 直接抨动 max 到上限 → 才走队列
Go:       无中间者、直接起 goroutine
1
2
3

Java 的选择偏与“以队列为起动”。有个反后果:LinkedBlockingQueue 默认无界 → 永不抨动 max → maximumPoolSize 设多大都没用。这是 Executors.newFixedThreadPool 事故根源。

# 7.2 任务调度策略

面对不同场景、Java 提供了两种提交哲学。

哲学①|直接提交 (Direct Handoff):走 SynchronousQueue 、任务不入队、最快响应

  场景:任务耗时较短、要求反转联云限低、可接受“快失败”
  例子:CachedThreadPool、高并发短任务场景
1
2

哲学②|队列提交 (Queueing):任务优先入队、队满才抨动 max

  场景:任务耗时不一、要求任务不能丢、接受轻锐延迟
  例子:FixedThreadPool、订单处理、计算任务、批棅场景
1
2

该选哪种? 三个提问决定:

1、任务可丢吗?             不可 → 队列提交
2、任务不一、生产突发?   是   → 队列提交
3、响应要求 < 100ms?       是   → 直接提交
1
2
3

# 7.3 拒绝策略

当线程池和队列都饱和时,采用以下策略之一:

策略 行为 适用场景 优缺点
AbortPolicy 抛出RejectedExecutionException 需要感知任务被拒绝 默认策略,简单直接
CallerRunsPolicy 调用者线程执行任务 降低任务提交速度 提供降级机制,但可能阻塞调用者
DiscardPolicy 静默丢弃任务 任务丢失可接受 简单,但任务会丢失
DiscardOldestPolicy 丢弃最老的任务 新任务优先级更高 保证新任务执行,但老任务丢失

# 08.添加线程任务

# 8.1 任务提交接口

线程池提供submit或execute方法用于提交任务。

# 8.2 任务封装

任务通常被封装为Runnable或Callable对象。Callable可以返回结果或抛出异常,而Runnable没有返回值。

# 8.3 任务提交示例

// Java示例
ExecutorService executor = Executors.newFixedThreadPool(5);
Future<String> future = executor.submit(new Callable<String>() {
    public String call() {
        return "任务结果";
    }
});
String result = future.get(); // 获取结果
1
2
3
4
5
6
7
8

# 09.线程回收机制

# 9.1 线程回收时机

当线程空闲时间超过keepAliveTime,且当前线程数大于核心线程数时,该线程会被终止。

当线程池关闭时,所有线程会被中断。

# 9.2 线程回收实现

线程池通过维护线程的空闲时间,当超过keepAliveTime时,线程会从任务队列获取任务时超时,然后线程退出。

# 9.3 线程回收示例

public void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    while (task != null || (task = getTask()) != null) {
        // 执行任务
        task.run();
        task = null;
    }
    // 如果没有任务,线程退出
}

private Runnable getTask() {
    boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
    try {
        Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take();
        if (r != null) return r;
    } catch (InterruptedException retry) {
        // 处理中断
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 9.4 线程池监控设计

线程池在生产环境中需要完善的监控机制,确保运行状态可观测:

核心监控指标

指标 含义 告警阈值参考
activeCount 当前活跃线程数 接近maximumPoolSize
poolSize 当前线程池大小 异常增长
queueSize 等待队列长度 超过容量的80%
completedTaskCount 已完成任务数 增长停滞
rejectedCount 拒绝任务数 大于0

监控实现方案

public class ThreadPoolMonitor {
    private final ThreadPoolExecutor executor;
    
    public void printStats() {
        System.out.println("活跃线程: " + executor.getActiveCount());
        System.out.println("池大小: " + executor.getPoolSize());
        System.out.println("队列长度: " + executor.getQueue().size());
        System.out.println("完成任务: " + executor.getCompletedTaskCount());
    }
}
1
2
3
4
5
6
7
8
9
10

# 9.5 线程池设计总结

线程池的核心价值

  1. 资源复用:避免频繁创建销毁线程的开销
  2. 流量控制:通过队列和最大线程数限制并发量
  3. 统一管理:集中管理线程的生命周期

设计原则

  • 根据任务类型(CPU密集/IO密集)选择合适的线程数
  • 选择合适的队列类型和容量
  • 配置合理的拒绝策略
  • 不同业务使用独立的线程池,实现故障隔离
  • 完善监控和告警,及时发现异常

常见陷阱

  • 使用无界队列导致OOM
  • 线程池过大导致上下文切换开销
  • 共用线程池导致业务互相影响
  • 忽略异常处理导致任务静默失败

# 10.跨语言设计哲学对比

线程池看似是个“全语言都一样”的东西,但实际上不同语言的实现哲学差别巨大。

# 10.1 Java:7 参数设计的哲学

new ThreadPoolExecutor(
    corePoolSize,           // 1、核心线程数
    maximumPoolSize,        // 2、最大线程数
    keepAliveTime,          // 3、空闲存活时间
    TimeUnit.SECONDS,       // 4、时间单位
    workQueue,              // 5、任务队列
    threadFactory,          // 6、线程工厂
    rejectedHandler         // 7、拒绝策略
);
1
2
3
4
5
6
7
8
9

Java 的哲学是“提供全部旋钮、主动权交给使用者”。七个参数能组合出几十种不同线程池表现:

corePoolSize=N、maxPoolSize=N、无界队列           → FixedThreadPool
corePoolSize=0、maxPoolSize=∞、SynchronousQueue   → CachedThreadPool
corePoolSize=N、maxPoolSize=N、DelayQueue            → ScheduledThreadPool
1
2
3

三个 Executors 工厂方法其实是这几个参数组合的快捷方式。阿里 Java 开发规范明令禁用这些快捷方法——原因是它们有隐藏陕阱:LinkedBlockingQueue 默认容量是 Integer.MAX_VALUE(实质无界)、CachedThreadPool 的 max 也是 Integer.MAX_VALUE。

# 10.2 Go:为什么不需要线程池?

// Go 中不需要线程池、直接起 goroutine
for task := range tasks {
    go process(task)   // 100 万任务、100 万 goroutine、轻量可控
}
1
2
3
4

Go 的哲学是“goroutine 本身就是“轻量任务””:

  goroutine 初始栈 2KB、动态增长             → 100 万 goroutine 仅几 GB
  GMP 调度器抽象 G/M/P、在用户态调度        → 上下文切换不陷内核
  调度器本身就是“线程池 + 任务队列”         → 不需业务层再加一层
1
2
3

但 Go 仍然在某些场景需要“goroutine 池”:

// 场景:限制并发、起 100 万 goroutine 有问题、需要 worker pool
sem := make(chan struct{}, 100)   // 并发限 100
for task := range tasks {
    sem <- struct{}{}              // 获取名额
    go func(t Task) {
        defer func() { <-sem }()   // 释放名额
        process(t)
    }(task)
}
1
2
3
4
5
6
7
8
9

这不是线程池、是“信号量限并”——Go 用 channel 实现了过去要用线程池才能做的事。

# 10.3 Python:GIL下的两种线程池

# CPU 密集 → 进程池
from concurrent.futures import ProcessPoolExecutor
with ProcessPoolExecutor(max_workers=8) as executor:
    results = executor.map(heavy_compute, tasks)

# IO 密集 → 线程池
from concurrent.futures import ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=20) as executor:
    results = executor.map(http_request, urls)
1
2
3
4
5
6
7
8
9

Python 独有现象:GIL 使得多线程不能同时在多核上跳动。所以 CPU 密集任务用 ThreadPool 纯属白费、必须上 ProcessPoolExecutor。这是 Java/Go/C# 都不需要考虑的问题。

# 10.4 C#/.NET:Task+ThreadPool分层

// .NET 使用者几乎不直接接触线程池、只面向 Task
await Task.Run(() => HeavyCompute());
await Task.WhenAll(urls.Select(url => HttpClient.GetAsync(url)));
1
2
3

.NET 的哲学是“Task 是使用者接口、ThreadPool 是运行时隐藏”:

Task           ←— 使用者面向
  ↓
TaskScheduler  ←— 选择怎么调度
ThreadPool     ←— 运行时负责、使用者不需直接调用
1
2
3
4

这是最高抽象、也是最“不易出错”的设计——使用者根本遇不到 “FixedThreadPool vs CachedThreadPool” 的选型问题。

# 10.5 跨语言设计哲学表

语言 是否需要显式线程池 并发单元 调度器位置 设计哲学
Java 是 OS 线程 应用层 使用者选参数、高面向可控
C# 隐含(Task) OS 线程 运行时 隐藏、使用者只看 Task
Go 不需要 goroutine 运行时(GMP) 并发是语言一等公民
Python 是(且需区分进程/线程) OS 线程 应用层 GIL 下的补偿
JavaScript 不需要 事件循环 运行时 单线程 + IO 多路复用
Rust 是(tokio Runtime) 任务 库 零成本抽象

这是个三层设计选择谱:应用层、运行时、语言。越向上、使用者越轻松;越向下、使用者越需要理解。

# 11.经典陕阱与事故复盘

线程池是“看似简单、用错事故不断”的经典。这里集中拆解五个生产事故级别的陕阱。

# 11.1 事故①|newFixedThreadPool导致OOM

事故场景:某互联网公司的订单服务用 Executors.newFixedThreadPool(50) 接收订单,双十一 2000 并发量突增、处理不过来、队列不断增长、JVM OOM。

// 看 Executors.newFixedThreadPool 的实现
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads, 0L, MILLISECONDS,
        new LinkedBlockingQueue<Runnable>());   // 默认容量为 Integer.MAX_VALUE!
}
1
2
3
4
5

根因:LinkedBlockingQueue 默认容量是 21 亿、实际上是无界队列。任务涌入速率 > 处理速率 → 队列不受控地增长 → 吃光堆内存 → OOM。

对策:遵守阿里规范、永远使用 new ThreadPoolExecutor() 原始构造器,明确指定有界队列:

// ✅ 明确限制
new ThreadPoolExecutor(50, 100, 60L, SECONDS,
    new ArrayBlockingQueue<>(1000),     // 明确容量
    new ThreadFactoryBuilder()
        .setNameFormat("order-%d").build(),
    new ThreadPoolExecutor.CallerRunsPolicy());   // 拒绝后调用者跑、向上反压到业务层
1
2
3
4
5
6

# 11.2 事故②|共用线程池导致死锁

事故场景:某金融公司、主服务提交任务 A 到公共线程池、A 里面又提交任务 B 到同一个线程池并 .get() 等 B。高峰期、线程池被 A 充满占完、所有 B 任务在队列里、A 在等 B、服务雪崩。

// ❌ 嵌套提交同一线程池、且同步等待
Future<B> b = pool.submit(() -> taskB());
pool.submit(() -> {
    Future<C> c = pool.submit(() -> taskC());   // 递归提交
    c.get();   // 等 c 运行、但 c 还在队列里、发生死锁
});
1
2
3
4
5
6

根因:线程池本质是个“有限资源”。在“同一个线程池里递归提交 + 同步等待”是决定性的死锁陕阱。

对策:

  • 不同业务使用独立线程池、避免互相影响;
  • 避免“提交 + 同步等待”模式、使用 CompletableFuture.thenCompose 等异步组合;
  • 进行压测验证最差场景。

# 11.3 事故③|任务异常被吞掉

// ❌ 调 submit、异常被包装到 Future、你不调 future.get() 就永远看不见
executor.submit(() -> {
    throw new RuntimeException("隐藏错误");
});
// 某公司事故:后台任务全静默失败、告警不出、进而事故
1
2
3
4
5

根因:submit 返回 Future、异常被包装进去了、你不调 .get() 你什么都不知道。这与 CompletableFuture 的隐藏错误事故同源。

对策:使用 execute 代替 submit(错误会被 UncaughtExceptionHandler 捕获)、或包装一层 try/catch:

executor.execute(() -> {
    try { actualWork(); }
    catch (Exception e) { log.error("任务失败", e); metrics.fail(); }
});
1
2
3
4

# 11.4 事故④|ThreadLocal不清理泄露

// ❌ ThreadLocal 不清理 + 线程池复用 = 内存泄露
static ThreadLocal<UserContext> CURRENT_USER = new ThreadLocal<>();

executor.execute(() -> {
    CURRENT_USER.set(new UserContext("Alice"));
    doWork();
    // 未 remove!线程下一个任务 set Bob 前、CURRENT_USER 仍然是 Alice
    // 更严重的是:Alice 对象不被 GC、内存泄露
});
1
2
3
4
5
6
7
8
9

根因:线程复用!不清理 ThreadLocal 会跨不同任务。生产环境会表现为老年代永不释放、Full GC 频繁。

对策:使用 try/finally 重点释放 ThreadLocal、或使用 InheritableThreadLocal、或 TransmittableThreadLocal(阿里开源)。

# 11.5 事故⑤|CallerRunsPolicy反锁主线程

事故场景:服务中报着拒绝策略 = CallerRunsPolicy、认为“调用者跑”是反压。但调用者是 Tomcat 处理请求的线程、这里被锁在业务逻辑上、后面全部请求被压。

根因:CallerRunsPolicy 的本质是“把拒绝代价付出去调用者”——调用者是谁决定这个策略是否适用:

调用者是 Tomcat 请求线程      → 不适用!会锁住全部请求
调用者是定时任务线程           → 不适用!会拖住后面定时
调用者是主任务独立的 worker  → 适用、是反压的理想场景
1
2
3

对策:默认使用 AbortPolicy + 反压上报、或自定义拒绝策略:

new RejectedExecutionHandler() {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        log.warn("任务被拒、走降级策略");
        metrics.rejected();
        try { fallbackQueue.offer(r, 100, MILLISECONDS); }   // 动态滑量
        catch (InterruptedException ie) { Thread.currentThread().interrupt(); }
    }
};
1
2
3
4
5
6
7
8
9

# 12.一句话总结

线程池本质上是“生产者-消费者”模式在资源有限下的商业营备营制哲学、提供一个可控、可观测、可恢复的并发边界。

三层认知:

表层抽象:一句话 —— "预创建一批线程、任务来了拾起跑"
中层抽象:三件套 —— "核心线程 + 任务队列 + 临时线程 + 拒绝策略"
底层抽象:五原则 —— "业务隔离、有界队列保护、反压上报、异常不吞、运行可视化"
1
2
3

终极建议:

  • 拒用 Executors 快捷方法——使用原始构造器、所有参数明确;
  • 业务隔离——不同业务独立线程池、故障不互相冲击;
  • 必须可观测——同机上报 activeCount/queueSize/rejectedCount、超过阈值报警;
  • 考虑虚拟线程——JDK 21+ 的 Loom 让 "一任务一线程" 成为可能、线程池在虚拟线程场景中逐步退出主要应用。

# 📎 延伸阅读

  • 前一篇:24.Actor与CSP并发模型(并发范式之边界)
  • 并发同源:19.并发上下文切换原理(为什么资源复用能节省这么高的开销)
  • 设计思想:23.协程核心设计思想(虚拟线程与协程让十万任务不需要"池")
  • 性能考虑:12.线程通信设计思想(为什么任务队列是同步起点)
上次更新: 2026/06/07, 10:26:12
14.Actor与CSP并发模型
16.线程池设计核心原理

← 14.Actor与CSP并发模型 16.线程池设计核心原理→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式