Skip to content

08.4 — Executor 与线程池

定位: 生产环境线程管理的标准方案 — 复用线程、控制并发、避免资源耗尽 面试高频度: ⭐⭐⭐⭐⭐ 考查方式: 核心参数、执行流程、拒绝策略、Executors 陷阱、线程池大小设置

一、这是什么?为什么需要它?

为什么需要线程池?

java
// 不用线程池:来一个任务创建一个线程
while (true) {
    Socket socket = server.accept();
    new Thread(() -> handle(socket)).start();
    // 问题:高并发下创建大量线程 → OOM / CPU 耗尽
}

线程池解决了 3 个问题:

1. 复用线程 — 避免频繁创建/销毁(线程创建 ≈ 1ms 级开销)
2. 控制并发数 — 防止无限线程导致资源耗尽
3. 管理生命周期 — 统一管理线程的创建、执行、销毁

二、原理拆解

2.1 ThreadPoolExecutor 核心参数

java
public ThreadPoolExecutor(
    int corePoolSize,        // ① 核心线程数
    int maximumPoolSize,     // ② 最大线程数
    long keepAliveTime,      // ③ 空闲线程存活时间
    TimeUnit unit,           // ④ 时间单位
    BlockingQueue<Runnable> workQueue, // ⑤ 工作队列
    ThreadFactory threadFactory,       // ⑥ 线程工厂(可选)
    RejectedExecutionHandler handler   // ⑦ 拒绝策略(可选)
);
参数详解:

① corePoolSize — 核心线程数
   即使空闲也保留的线程数(除非 allowCoreThreadTimeOut=true)

② maximumPoolSize — 最大线程数
   线程池允许的最大线程数

③ keepAliveTime — 空闲存活时间
   超过核心线程数的空闲线程的最长存活时间

④ workQueue — 工作队列
   等待执行的任务队列
   常见队列:ArrayBlockingQueue / LinkedBlockingQueue / SynchronousQueue

⑤ handler — 拒绝策略
   当线程池和队列都满时,如何处理新提交的任务

2.2 执行流程

┌─────────────────────────────────────────────────────────────┐
│               ThreadPoolExecutor 执行流程                     │
│                                                              │
│  submit(task)                                                │
│       │                                                      │
│       ▼                                                      │
│  ① 当前线程数 < corePoolSize?                                │
│     ┌──┴──┐                                                  │
│    YES    NO                                                 │
│     │      │                                                 │
│     ▼      ▼                                                 │
│  新建核心线程  ② 队列未满?                                   │
│  执行任务      ┌──┴──┐                                       │
│              YES    NO                                       │
│               │      │                                       │
│               ▼      ▼                                       │
│           入队等待   ③ 当前线程数 < maxPoolSize?              │
│                      ┌──┴──┐                                 │
│                     YES    NO                                │
│                      │      │                                │
│                      ▼      ▼                                │
│                   新建线程   ④ 执行拒绝策略                   │
│                   执行任务                                    │
│                                                              │
│  ⚡ 关键:是先入队,再创建非核心线程!                          │
│  很多人误以为"核心 → 最大 → 队列",实际上是:                   │
│  核心 → 队列 → 最大 → 拒绝                                    │
└─────────────────────────────────────────────────────────────┘
java
// 代码验证执行流程
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2,      // core
    5,      // max
    60, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(3)  // 队列容量 3
);

// 提交 8 个任务
for (int i = 1; i <= 8; i++) {
    executor.submit(() -> {
        Thread.sleep(Long.MAX_VALUE);  // 让线程一直运行
    });
}
// 第 1-2 个:新建核心线程执行
// 第 3-5 个:入队等待(队列容量 3)
// 第 6-7 个:新建非核心线程执行(pool: 2→4→5)
// 第 8 个:执行拒绝策略

2.3 四种拒绝策略

策略行为适用场景
AbortPolicy (默认)抛 RejectedExecutionException必须处理的场景
CallerRunsPolicy由提交任务的线程执行慢速降级,让调用方慢下来
DiscardPolicy静默丢弃不重要的任务
DiscardOldestPolicy丢弃队列中最旧的任务,重试提交新任务比旧任务重要
java
// CallerRunsPolicy 示例
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2, 5, 60, SECONDS,
    new ArrayBlockingQueue<>(10),
    new ThreadPoolExecutor.CallerRunsPolicy()
    // 当线程池满时,提交任务的线程自己执行任务
    // 效果:任务处理速度 = 提交速度,自然降级
);

2.4 Executors 工厂方法的陷阱

java
// ❌ 陷阱 1:无界队列
ExecutorService pool = Executors.newFixedThreadPool(10);
// 内部使用 LinkedBlockingQueue (无界)
// 如果任务提交速度 >> 处理速度 → 队列无限增长 → OOM

// ❌ 陷阱 2:最大线程无限
ExecutorService pool = Executors.newCachedThreadPool();
// core=0, max=Integer.MAX_VALUE
// 高并发下创建大量线程 → OOM (线程栈内存)

// ❌ 陷阱 3:单线程队列无界
ExecutorService pool = Executors.newSingleThreadExecutor();
// 同样使用无界 LinkedBlockingQueue

// ✅ 最佳实践:手动创建 ThreadPoolExecutor
ThreadPoolExecutor pool = new ThreadPoolExecutor(
    10,
    20,
    60L, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(100),  // 有界队列!
    new ThreadPoolExecutor.CallerRunsPolicy()
);

2.5 线程池大小设置

通用公式(经验值):

CPU 密集型(计算为主):
  Nthreads = CPU核心数 + 1(多一个防止缺页中断)

IO 密集型(等待为主):
  Nthreads = CPU核心数 * (1 + 平均等待时间 / 平均计算时间)
  
  如果 IO 等待时间是计算时间的 10 倍:
  Nthreads = 4核 * (1 + 10) = 44

实际建议:
  - 压测调整,没有绝对公式
  - 从保守值开始,逐步增加
  - 监控队列积压、线程活跃度

三、图解全景

┌─────────────────────────────────────────────────────────────┐
│             ThreadPoolExecutor 内部结构                       │
│                                                              │
│  ┌──────────────────────────────────────────────────────┐   │
│  │             任务提交流程                              │   │
│  │                                                      │   │
│  │  ┌─────────┐  ①               ②             ③       │   │
│  │  │  任务   │ ───→ corePool ───→ workQueue ───→ maxPool│   │
│  │  └─────────┘     (运行中)      (等待中)     (运行中)  │   │
│  │       │                                               │   │
│  │       │ ④ 队列满 + 达到最大                           │   │
│  │       └──→ handler (拒绝策略)                          │   │
│  └──────────────────────────────────────────────────────┘   │
│                                                              │
│  ┌──────────────────────────────────────────────────────┐   │
│  │             线程池内部状态                              │   │
│  │                                                      │   │
│  │  RUNNING → SHUTDOWN(不再接收) → STOP(立即停止)       │   │
│  │  → TIDYING(线程为0) → TERMINATED                     │   │
│  │                                                      │   │
│  │  shutdown()  vs  shutdownNow()                       │   │
│  │  优雅关闭  vs  立即关闭(返回未执行任务列表)           │   │
│  └──────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

四、实战验证

4.1 不同拒绝策略效果

java
// 配置:core=1, max=2, queue=1
// 提交 4 个任务

// AbortPolicy:第 4 个任务抛异常
// CallerRunsPolicy:第 4 个任务由 main 线程执行
// DiscardPolicy:第 4 个任务静默丢弃
// DiscardOldestPolicy:丢弃队列中等待的任务,第 4 个任务入队

4.2 监控线程池

java
ThreadPoolExecutor pool = new ThreadPoolExecutor(...);

// 定期监控
ScheduledExecutorService monitor = Executors.newScheduledThreadPool(1);
monitor.scheduleAtFixedRate(() -> {
    System.out.println("PoolSize: " + pool.getPoolSize());
    System.out.println("ActiveCount: " + pool.getActiveCount());
    System.out.println("QueueSize: " + pool.getQueue().size());
    System.out.println("CompletedTaskCount: " + pool.getCompletedTaskCount());
}, 0, 5, TimeUnit.SECONDS);

五、面试视角

追问答案要点
线程池的执行顺序?core → queue → max → 拒绝(很多人答错成 core → max → queue)
为什么禁止 Executors 创建线程池?无界队列/无界线程 → OOM 风险
线程池大小怎么设置?CPU 密集型 N+1;IO 密集型 2N;实际靠压测
shutdown() 和 shutdownNow() 区别?shutdown 优雅停止不接受新任务;shutdownNow 中断正在运行 + 返回未执行列表
核心线程数能动态调整吗?setCorePoolSize() 和 setMaximumPoolSize() 可在运行时调整

📚 相关链接

  • **线程基础与创建** — 线程基础
  • **线程安全与锁** — 线程安全
  • **并发工具类(AQS)** — 并发工具
  • ← 返回 **并发索引**

Knowledge4J — Java 知识库