ThreadPoolExecutor简单介绍

101 阅读10分钟

知其然要知其所以然,探索每一个知识点背后的意义,你知道的越多,你不知道的越多,一起学习,一起进步,如果文章感觉对您有用的话,关注、收藏、点赞,有困惑的地方可以评论,我们一起探讨!

1. 概述

  • 定义ThreadPoolExecutor 是 Java 并发编程中用于管理线程池的核心类,属于 java.util.concurrent 包。它通过复用线程、减少线程创建和销毁的开销,提高了多线程任务的执行效率。
  • 背景:在高并发场景中,频繁创建和销毁线程会消耗大量系统资源。线程池通过预先创建一定数量的线程并复用它们,显著提升了系统性能和资源利用率。

2. 核心作用

  • 降低资源消耗:通过复用已创建的线程,减少线程创建和销毁的开销。
  • 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
  • 提高线程的可管理性:线程是稀缺资源,线程池可以统一管理线程的分配、调优和监控,避免无限制创建线程导致的系统资源耗尽和稳定性问题。

3. 线程池的生命周期

ThreadPoolExecutor 使用一个 AtomicInteger 类型的变量(ctl)来同时表示线程池的状态(runState)和线程池中的工作线程数量(workerCount),底层实现是CAS。ctl 的高 3 位表示线程池状态,低 29 位表示线程数量。这种设计避免了多变量之间的不一致性问题,同时减少了锁的使用,提升了性能。

线程池的 5 种状态

  1. RUNNING(运行状态)

    • 状态值RUNNING = 0
    • 描述:线程池处于正常运行状态,可以接收新任务并处理队列中的任务。
    • 行为
      • 接受新任务。
      • 处理工作队列中的任务。
  2. SHUTDOWN(关闭状态)

    • 状态值SHUTDOWN = 1
    • 描述:线程池不再接受新任务,但会继续处理工作队列中已存在的任务。
    • 触发条件:调用 shutdown() 方法。
    • 行为
      • 拒绝新任务。
      • 继续处理工作队列中的任务。
      • 中断空闲线程。
  3. STOP(停止状态)

    • 状态值STOP = 2
    • 描述:线程池不再接受新任务,也不会处理工作队列中的任务,并尝试中断所有正在执行的任务。
    • 触发条件:调用 shutdownNow() 方法。
    • 行为
      • 拒绝新任务。
      • 清空工作队列。
      • 中断所有线程(包括正在执行任务的线程)。
  4. TIDYING(整理状态)

    • 状态值TIDYING = 3
    • 描述:线程池中的任务已全部终止,工作线程数为 0,准备执行终止后的钩子方法。
    • 触发条件
      • 当线程池从 SHUTDOWN 状态转换而来,且工作队列为空且工作线程数为 0。
      • 当线程池从 STOP 状态转换而来,且工作线程数为 0。
    • 行为
      • 调用 terminated() 钩子方法。
  5. TERMINATED(终止状态)

    • 状态值TERMINATED = 4
    • 描述:线程池完全终止,所有资源已释放。
    • 触发条件terminated() 方法执行完毕。
    • 行为
      • 线程池完全停止运行。

状态转换图

stateDiagram
    [*] --> RUNNING
    RUNNING --> SHUTDOWN: 调用shutdown()
    RUNNING --> STOP: 调用shutdownNow()
    SHUTDOWN --> TIDYING: 队列和线程池为空
    STOP --> TIDYING: 线程池为空
    TIDYING --> TERMINATED: terminated()执行完毕
    TERMINATED --> [*]

状态转换的触发方法

  • shutdown()
    • 将线程池状态从 RUNNING 转换为 SHUTDOWN
    • 不再接受新任务,但会继续处理队列中的任务。
  • shutdownNow()
    • 将线程池状态从 RUNNINGSHUTDOWN 转换为 STOP
    • 不再接受新任务,清空队列,并尝试中断所有线程。
  • terminated()
    • 当线程池状态变为 TIDYING 时,会自动调用此方法。
    • 开发者可以重写此方法,执行一些自定义的清理操作。
  • awaitTermination(long timeout, TimeUnit unit)
    • 等待线程池状态变为 TERMINATED,或者超时。

示例代码

import java.util.concurrent.*;

public class ThreadPoolLifecycleExample {
    public static void main(String[] args) throws InterruptedException {
        // 创建线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2, // 核心线程数
                4, // 最大线程数
                60, // 空闲线程存活时间
                TimeUnit.SECONDS, // 时间单位
                new LinkedBlockingQueue<>(10) // 工作队列
        {
            @Override
            protected void terminated() {
                System.out.println("线程池已终止!");
            }
        };

        // 提交任务
        for (int i = 0; i < 10; i++) {
            int taskId = i;
            executor.execute(() -> {
                System.out.println("任务 " + taskId + " 正在执行,线程: " + Thread.currentThread().getName());
                try {
                    Thread.sleep(1000); // 模拟任务执行
                } catch (InterruptedException e) {
                    System.out.println("任务 " + taskId + " 被中断!");
                }
            });
        }

        // 关闭线程池
        executor.shutdown(); // 转换为 SHUTDOWN 状态
        System.out.println("线程池已调用 shutdown()");

        // 等待线程池终止
        if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
            System.out.println("线程池未完全终止,调用 shutdownNow()");
            executor.shutdownNow(); // 转换为 STOP 状态
        }

        System.out.println("主线程结束");
    }
}

4. 参数详解

核心线程数(corePoolSize)

  • 作用:线程池中保持存活的最小线程数,即使这些线程处于空闲状态。
  • 默认行为
    • 线程池初始化时不会立即创建核心线程,而是在有任务提交时逐步创建(懒加载思想,不过也可以调用prestartAllCoreThreads()方法启动所有核心线程,这个避免了首次任务的等待时间:消除冷启动延迟,视业务情况决定是否调用,如果设置了 allowCoreThreadTimeOut=true,预启动的线程可能在空闲超时后被销毁,这个要注意下)。
    • 如果设置了 allowCoreThreadTimeOut(true),默认是false,核心线程在空闲超时后也会被回收,当是false的时候,核心线程永不销毁,除非线程池被关闭(shutdown()/shutdownNow()
  • 适用场景
    • 适合长期保持一定数量线程的场景。
  • 核心线程为什么长期存活?
    • 避免冷启动延迟:线程创建需要系统资源,频繁创建/销毁会导致性能抖动。
    • 应对突发流量:保留核心线程可快速处理新任务,无需等待线程创建。
    • 资源与响应平衡:牺牲少量常驻内存,换取稳定的吞吐量

最大线程数(maximumPoolSize)

  • 作用:线程池中允许存在的最大线程数量。
  • 默认行为
    • 当任务数量超过核心线程数且工作队列已满时,线程池会创建新线程,直到达到最大线程数,如果线程等待任务的时间超过了keepAliveTime,就会被回收,系统是通过addWorker 方法的 core 参数来区分核心线程和非核心线程的
  • 适用场景
    • 适合任务量波动较大的场景。

工作队列(workQueue)

  • 作用:用于存放等待执行的任务的队列。
  • 常见队列类型
    • LinkedBlockingQueue:无界队列,适合任务量较大的场景。
    • ArrayBlockingQueue:有界队列,适合需要控制队列大小的场景。
    • SynchronousQueue:不存储任务的队列,适合任务量较小且需要快速响应的场景。
    • PriorityBlockingQueue:支持优先级排序的无界队列。

线程工厂(threadFactory)

  • 作用:用于创建新线程的工厂。
  • 默认行为
    • 使用 Executors.defaultThreadFactory(),创建的线程具有相同的优先级和非守护线程状态。
  • 自定义用途
    • 可以自定义线程的名称、优先级、是否为守护线程等。

拒绝策略(RejectedExecutionHandler)

  • 作用:当线程池无法接受新任务时,用于处理被拒绝任务的策略。
  • 常见策略
    • AbortPolicy:直接抛出 RejectedExecutionException 异常。
    • CallerRunsPolicy:由提交任务的线程直接执行该任务。
    • DiscardPolicy:直接丢弃被拒绝的任务。
    • DiscardOldestPolicy:丢弃队列中最旧的任务,然后重新尝试提交当前任务。
    • 也可自定义拒绝策略,可以接入告警机制,例如钉钉、企微告警等

5. 工作原理

任务执行流程

  1. 任务提交
    • 当有新任务提交时,线程池会优先使用核心线程执行任务。
  2. 任务调度
    • 如果核心线程都在忙碌,任务会被放入工作队列中等待执行。
    • 如果工作队列已满且线程数未达到最大线程数,线程池会创建新线程来执行任务。
    • 如果线程数已达到最大线程数且队列已满,线程池会触发拒绝策略处理新任务。
  3. 线程复用
    • 线程池中的线程在执行完任务后不会被销毁,而是保持存活状态,等待下一个任务。
  4. 线程回收
    • 当线程池中的线程数超过核心线程数时,空闲线程在 keepAliveTime 超时后会被回收。

流程图

sequenceDiagram
    participant Client as 客户端
    participant ThreadPool as 线程池
    participant CoreThread as 核心线程
    participant Queue as 工作队列
    participant NewThread as 新线程
    participant RejectPolicy as 拒绝策略

    Client->>ThreadPool: 提交任务
    alt 是否有空闲核心线程?
        ThreadPool->>CoreThread: 使用核心线程执行任务
        CoreThread-->>ThreadPool: 任务完成
    else 工作队列是否已满?
        ThreadPool->>Queue: 将任务放入工作队列
        Queue-->>ThreadPool: 任务排队
    else 线程数是否达到最大线程数?
        ThreadPool->>NewThread: 创建新线程执行任务
        NewThread-->>ThreadPool: 任务完成
    else
        ThreadPool->>RejectPolicy: 触发拒绝策略
        RejectPolicy-->>Client: 处理被拒绝的任务
    end

6. 线程池的创建方式

1. 固定大小的线程池(FixedThreadPool)

  • 特点:线程数量固定,任务队列无界。
  • 适用场景:适合任务量稳定且需要限制线程数量的场景。
  • 创建方法
    ExecutorService executor = Executors.newFixedThreadPool(int nThreads);
    

2. 单线程的线程池(SingleThreadExecutor)

  • 特点:只有一个线程,任务队列无界。
  • 适用场景:适合需要保证任务顺序执行的场景。
  • 创建方法
    ExecutorService executor = Executors.newSingleThreadExecutor();
    

3. 可缓存的线程池(CachedThreadPool)

  • 特点:线程数量不固定,空闲线程在 60 秒后被回收。
  • 适用场景:适合任务量波动较大的场景。
  • 创建方法
    ExecutorService executor = Executors.newCachedThreadPool();
    

4. 定时任务的线程池(ScheduledThreadPool)

  • 特点:支持定时任务和周期性任务。
  • 适用场景:适合需要定时执行或周期性执行任务的场景。
  • 创建方法
    ScheduledExecutorService executor = Executors.newScheduledThreadPool(int corePoolSize);
    

5. 自定义线程池

  • 特点:通过 ThreadPoolExecutor 构造函数显式设置线程池参数。
  • 适用场景:适合需要灵活配置线程池参数的场景。
  • 创建方法
    ThreadPoolExecutor executor = new ThreadPoolExecutor(
        corePoolSize,
        maximumPoolSize,
        keepAliveTime,
        unit,
        workQueue,
        threadFactory,
        handler
    );
    

7. Work

  • work是什么?
    • Worker是线程池中封装工作线程的类,负责执行任务,并管理线程的生命周期。每个Worker对应一个线程,处理队列中的任务
  • work的核心职责
    • 线程和任务绑定:
      • 每个 Worker 持有一个由线程工厂(ThreadFactory)创建的线程,该线程实际执行的是 Worker 自身的 run() 方法。
      • 当通过 addWorker(firstTask, core) 创建 Worker 时,若 firstTask 非 null,线程会优先执行此任务,之后再从队列中获取任务。
    • 循环执行任务: 在 runWorker(Worker w) 方法中,Worker 通过以下流程执行任务: 1、优先执行 firstTask(如果存在) 2、调用 getTask() 【这个方法是有超时时间的】从队列中获取新任务(可能阻塞或超时)。 3、若 getTask() 返回 null,则退出循环,触发线程回收。
    • 锁机制(AQS): 通过继承 AbstractQueuedSynchronizer(AQS),Worker 实现了一个不可重入的独占锁,用于防止任务执行期间被中断(当线程正在执行任务时(调用 task.run()),锁被持有,此时中断请求会被暂存,直到任务完成)、线程安全的状态管理(确保 Worker 的中断和状态变更操作是线程安全的)
final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); // 允许中断
    boolean completedAbruptly = true;
    try {
        // 循环获取任务并执行
        while (task != null || (task = getTask()) != null) {
            w.lock(); // 加锁表示任务正在执行
            try {
                beforeExecute(wt, task); // 钩子方法(可重写)
                task.run();             // 执行任务
                afterExecute(task, null); // 钩子方法(可重写)
            } finally {
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly); // 处理线程退出
    }
}

8. 思考

  • ThreadPoolExecutor 如何维护自身状态?
    • 通过 ctl 变量维护线程池的状态和工作线程数量,使用位运算高效管理状态转换。
  • ThreadPoolExecutor 如何维护内部线程?
    • 通过 Worker 类封装线程和任务,使用 HashSet<Worker> 维护工作线程集合。
  • ThreadPoolExecutor 如何提交任务?
    • 通过 execute(Runnable command) 方法提交任务,根据线程池状态和参数决定任务的执行方式。