跳转到内容

JUC线程池-ThreadPoolExecutor

草稿难度:中级#JUC

原文:CSDN(历史文章导入,当前状态为草稿)

在探讨ThreadPoolExecutor的细节之前,我们首先要理解为什么在Java并发编程中,线程池扮演着如此重要的角色。

想象一下,如果你的应用程序需要处理大量并发请求,例如一个Web服务器。对于每个进来的请求,你都创建一个新的线程去处理:

// 伪代码:为每个请求创建新线程
while (true) {
Socket connection = serverSocket.accept();
new Thread(() -> handleRequest(connection)).start();
}

这种简单粗暴的方式会带来一系列严重的问题:

  1. 高昂的资源开销:线程的创建和销毁并非“免费午餐”。它们涉及与操作系统的交互,需要分配内存、初始化线程栈、进行系统调用等,这些操作都会消耗宝贵的CPU时间和内存资源。如果请求量巨大,频繁创建销毁线程将成为性能瓶颈。
  2. 资源耗尽风险:如果不加限制地创建线程,当并发量过高时,可能会耗尽系统的内存或CPU资源,导致应用程序崩溃(如OutOfMemoryError),甚至拖垮整个操作系统。每个线程都需要一定的内存空间(线程栈),过多的线程会迅速消耗内存。
  3. 性能抖动与上下文切换:大量的活跃线程会导致CPU在不同线程之间频繁切换(上下文切换),这本身也有相当大的开销。过多的上下文切换会浪费CPU时间,降低实际处理任务的效率。
  4. 缺乏统一管理:手动创建的线程难以进行统一的管理、监控和调优。你很难知道当前有多少线程在运行,它们的状况如何,以及如何优雅地控制它们的生命周期。

为了解决上述问题,“池化”(Pooling)技术应运而生。线程池的核心思想就是复用已经创建的线程,避免频繁创建和销毁带来的开销。

想象一个工厂的流水线,工厂不会为每个零件都雇佣一个新工人,然后完工后解雇他。(如果这样做就很逆天了)
而是会雇佣一批固定或有上限的工人(线程),让他们待在工厂里(线程池)。当有零件需要加工时(任务来了),就从空闲的工人中找一个去处理。如果所有工人都在忙,新的零件就在传送带上排队等待(任务队列)。
如果传送带也满了,工厂可能临时加雇一些工人(非核心线程),但工人总数不能超过工厂的最大容量。
如果连临时工都满了,工厂就只能拒绝接收新的零件(拒绝策略)。当零件加工高峰期过去,临时工没事干了就会被解雇(非核心线程回收)。

这就是线程池的基本运作模式:

  • 预创建线程:在线程池启动时,可以预先创建一定数量的线程(核心线程),让它们处于等待任务的状态。
  • 任务队列:当所有工作线程都在忙碌时,新提交的任务会被放入一个队列中缓存起来。
  • 线程复用:工作线程执行完一个任务后,不会立即销毁,而是会去任务队列中尝试获取下一个任务继续执行。
  • 弹性伸缩:线程池可以根据负载情况,在核心线程数和最大线程数之间动态调整实际运行的线程数量。
  • 统一管理:线程池提供了管理线程生命周期、监控线程状态、调整参数等功能。

ThreadPoolExecutor是Java并发包(java.util.concurrent,简称JUC)提供的最核心、最灵活的线程池实现。

2.1 ThreadPoolExecutor在并发包中的位置

Section titled “2.1 ThreadPoolExecutor在并发包中的位置”

JUC包提供了一个强大的并发框架,而ThreadPoolExecutor是这个框架的基石之一。它位于java.util.concurrent包下,是构建高性能、高并发应用的关键组件。

2.2 Executor, ExecutorService, ThreadPoolExecutor的关系

Section titled “2.2 Executor, ExecutorService, ThreadPoolExecutor的关系”

理解这三者之间的关系对于掌握Java并发框架至关重要。它们构成了一个清晰的接口与实现层次:

  1. Executor接口

    • 位于顶层,是执行已提交Runnable任务的对象的基础接口。
    • 只定义了一个核心方法:void execute(Runnable command)
    • 它的设计目的是将任务的提交与任务的执行解耦。调用者只需要关心如何提交任务,而不需要关心任务具体是如何被执行的(是新建线程执行?还是在当前线程执行?还是由线程池执行?)。
    public interface Executor {
    void execute(Runnable command);
    }
  2. ExecutorService接口

    • 继承自Executor接口,在其基础上进行了扩展。
    • 增加了更完善的线程池生命周期管理功能(如shutdown(), shutdownNow(), isShutdown(), isTerminated(), awaitTermination())。
    • 增加了提交带有返回值的任务(Callable)以及获取任务执行结果(Future)的能力(如submit()系列方法)。
    • 提供了更丰富的任务提交方式(如invokeAny(), invokeAll())。
    public interface ExecutorService extends Executor {
    void shutdown(); // 优雅关闭
    List<Runnable> shutdownNow(); // 立即关闭,返回未执行任务
    boolean isShutdown(); // 是否已关闭
    boolean isTerminated(); // 是否已终止
    boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException; // 等待终止
    <T> Future<T> submit(Callable<T> task); // 提交Callable任务
    <T> Future<T> submit(Runnable task, T result); // 提交Runnable任务,并指定返回结果
    Future<?> submit(Runnable task); // 提交Runnable任务
    // ... 其他方法
    }
  3. ThreadPoolExecutor

    • ExecutorService接口最重要、最常用的具体实现类
    • 它提供了线程池所需的所有核心功能:线程创建、线程管理、任务排队、任务拒绝、生命周期控制等。
    • 它通过一系列可配置的参数(后面会详细介绍),允许开发者根据具体需求定制线程池的行为。
    public class ThreadPoolExecutor extends AbstractExecutorService {
    // 实现了ExecutorService接口的所有方法,并提供了具体的线程池逻辑
    // 包含 corePoolSize, maximumPoolSize, keepAliveTime, workQueue 等核心参数
    }

    (AbstractExecutorService是一个抽象类,提供了ExecutorService接口部分方法的默认实现,简化了具体实现类的编写,ThreadPoolExecutor继承了它。)

核心关系Executor 定义了执行任务的基本契约,ExecutorService 扩展了生命周期管理和Future支持,而 ThreadPoolExecutorExecutorService 的一个强大且可定制化的实现。

面向接口编程的好处:这种接口与实现分离的设计允许我们编写更灵活的代码。我们可以面向ExecutorService接口编程,在运行时根据需要选择不同的实现(虽然ThreadPoolExecutor是最常用的,但还有如ForkJoinPool, ScheduledThreadPoolExecutor等其他实现)。

// 面向接口编程
ExecutorService service = new ThreadPoolExecutor(...); // 或者 Executors.newFixedThreadPool(...)
// 或者
// ExecutorService service = Executors.newCachedThreadPool();
service.submit(() -> System.out.println("Task running"));
service.shutdown();

总结来说,ThreadPoolExecutor的核心作用就是:

  1. 资源管理:通过控制线程的数量(corePoolSize, maximumPoolSize),避免无限创建线程导致系统资源耗尽。
  2. 性能提升:通过复用已存在的线程,显著减少了线程创建和销毁的开销,提高了任务处理的响应速度。
  3. 并发控制:可以精确控制同时执行任务的线程数量,防止系统过载。
  4. 任务管理:提供了任务排队(workQueue)和任务拒绝(rejectedExecutionHandler)机制,使得任务处理更加有序和健壮。
  5. 简化开发:将线程管理和任务调度的复杂性封装起来,让开发者可以更专注于业务逻辑的实现。

3. 冻手冻手:创建你的第一个线程池

Section titled “3. 冻手冻手:创建你的第一个线程池”

了解了基本概念后,我们来动手创建一个ThreadPoolExecutor实例。

3.1 一个简单的ThreadPoolExecutor示例

Section titled “3.1 一个简单的ThreadPoolExecutor示例”

创建ThreadPoolExecutor最直接的方式是使用它的构造函数,传入核心参数:

import java.util.concurrent.*;
public class ThreadPoolDemo {
public static void main(String[] args) {
// 获取CPU核心数,作为线程池大小的参考
int corePoolSize = Runtime.getRuntime().availableProcessors();
int maximumPoolSize = corePoolSize * 2; // 示例:最大线程数为核心数的两倍
long keepAliveTime = 60L; // 非核心线程空闲存活时间
TimeUnit unit = TimeUnit.SECONDS; // 时间单位:秒
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100); // 有界队列,容量100
ThreadFactory threadFactory = Executors.defaultThreadFactory(); // 默认线程工厂
RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy(); // 默认拒绝策略:抛异常
// 创建ThreadPoolExecutor实例
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
threadFactory,
handler
);
System.out.println("线程池已创建,核心线程数:" + corePoolSize + ", 最大线程数:" + maximumPoolSize);
// 提交任务
for (int i = 0; i < 150; i++) { // 提交150个任务
final int taskId = i;
try {
executor.execute(() -> {
System.out.println(Thread.currentThread().getName() + " 正在执行任务 " + taskId);
try {
// 模拟任务执行耗时
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
System.out.println("任务 " + taskId + " 已提交");
} catch (RejectedExecutionException e) {
System.err.println("任务 " + taskId + " 被拒绝!" + e.getMessage());
// 这里可以根据业务需要处理被拒绝的任务,比如记录日志、放入备用队列等
}
}
// 关闭线程池(不再接受新任务,等待已提交任务执行完毕)
executor.shutdown();
try {
// 等待线程池终止,设置一个超时时间
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
System.err.println("线程池未能在规定时间内完全终止,尝试立即关闭...");
executor.shutdownNow(); // 尝试立即关闭
}
} catch (InterruptedException e) {
System.err.println("等待线程池终止时被打断,尝试立即关闭...");
executor.shutdownNow();
Thread.currentThread().interrupt();
}
System.out.println("所有任务执行完毕,线程池已关闭。");
}
}

在这个例子中:

  • 我们根据CPU核心数设置了corePoolSizemaximumPoolSize
  • 使用了容量为100的ArrayBlockingQueue作为任务队列。
  • 设置了非核心线程空闲60秒后被回收。
  • 使用了默认的线程工厂和拒绝策略(AbortPolicy)。
  • 我们提交了150个任务。由于核心线程+队列容量 (corePoolSize + 100) 小于150,且允许创建最大线程 (maximumPoolSize),线程池会先填满核心线程,然后填满队列,再创建非核心线程,最后可能会触发拒绝策略(如果 maximumPoolSize + workQueue.capacity < 150)。
  • 最后,我们调用shutdown()优雅地关闭线程池,并使用awaitTermination()等待其完成。

注意:实际项目中,参数的设置需要根据具体的业务场景、任务类型(CPU密集型/IO密集型)、系统资源等因素仔细评估,而不是简单地使用示例中的值。

3.2 使用Executors工厂类的便捷(当然还有它的风险)

Section titled “3.2 使用Executors工厂类的便捷(当然还有它的风险)”

为了简化ThreadPoolExecutor的创建,java.util.concurrent.Executors类提供了一些静态工厂方法:

  • Executors.newFixedThreadPool(int nThreads): 创建一个固定大小的线程池。
    • corePoolSize == maximumPoolSize == nThreads
    • keepAliveTime = 0L (因为没有非核心线程)
    • workQueue = new LinkedBlockingQueue<Runnable>() (无界队列!)
  • Executors.newSingleThreadExecutor(): 创建一个只有一个工作线程的线程池。
    • corePoolSize == maximumPoolSize == 1
    • keepAliveTime = 0L
    • workQueue = new LinkedBlockingQueue<Runnable>() (无界队列!)
  • Executors.newCachedThreadPool(): 创建一个可缓存的线程池,线程数量根据需要创建。
    • corePoolSize = 0
    • maximumPoolSize = Integer.MAX_VALUE (几乎无限制!)
    • keepAliveTime = 60L
    • workQueue = new SynchronousQueue<Runnable>() (不存储元素的队列)
  • Executors.newScheduledThreadPool(int corePoolSize): 创建一个支持定时及周期性任务执行的线程池。返回的是ScheduledThreadPoolExecutor实例。

便捷之处:使用这些工厂方法非常简单,只需要一两行代码就能创建一个线程池。

ExecutorService fixedPool = Executors.newFixedThreadPool(10);
ExecutorService cachedPool = Executors.newCachedThreadPool();

潜在风险(非常重要!)

  1. newFixedThreadPoolnewSingleThreadExecutor 的风险
    • 它们都使用了无界的LinkedBlockingQueue。如果任务提交的速度持续快于任务处理的速度,队列会无限增长,最终可能耗尽内存,导致OutOfMemoryError (OOM)。
  2. newCachedThreadPool 的风险
    • 它的maximumPoolSize被设置为Integer.MAX_VALUE。如果短时间内有大量任务涌入,并且任务执行时间稍长,线程池会创建大量的线程(理论上可达2^31-1个),这同样可能耗尽系统资源(内存或线程数限制),导致系统崩溃或OOM。

最佳实践
在阿里巴巴Java开发规约中,强制要求开发者不允许使用Executors去创建线程池,而是通过ThreadPoolExecutor的构造函数,这样可以让开发者更加明确线程池的运行规则,规避资源耗尽的风险。

【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
说明:使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。

【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 返回的线程池对象的弊端如下:
1)FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2)CachedThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

因此,强烈建议总是使用ThreadPoolExecutor的构造函数来创建线程池,并仔细配置每一个参数,尤其是workQueue的容量和maximumPoolSize

4. 核心参数深度剖析(七大护法了属于是)

Section titled “4. 核心参数深度剖析(七大护法了属于是)”

ThreadPoolExecutor的灵活性和强大功能主要源于其七个核心构造参数。理解透彻这些参数是精通线程池的关键。

public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)

让我们逐一深入解析:

  • 定义:线程池中长期保持活动的线程数量,即使它们处于空闲状态。
  • 核心角色:可以理解为线程池的“常驻员工”。它们是处理常规任务负载的主力军。
  • 创建时机:当新任务提交时,如果当前运行的线程数小于corePoolSize,线程池优先创建新的核心线程来处理任务,即使此时有其他核心线程是空闲的。这种策略是为了快速响应任务,避免从队列取任务的延迟。(注意:prestartCoreThread()prestartAllCoreThreads()可以预先启动核心线程)。
  • 回收策略:默认情况下,核心线程即使空闲也不会被回收。但如果调用了allowCoreThreadTimeOut(true)方法,那么核心线程在空闲时间超过keepAliveTime后也会被回收。

corePoolSize的设置需要权衡:

  • 太小:无法充分利用系统资源,任务处理可能较慢,队列容易积压。
  • 太大:即使系统空闲,也会占用较多资源(内存、CPU上下文切换开销)。

设置依据

  • 任务类型
    • CPU密集型任务(例如:复杂计算、加解密、压缩):这类任务主要消耗CPU资源,线程数过多会导致频繁的上下文切换,反而降低效率。通常建议设置为 CPU核心数 + 1。多的一个是为了防止线程因偶尔的页中断或其他原因阻塞时,CPU能得到充分利用。
    • IO密集型任务(例如:数据库操作、文件读写、网络请求):这类任务大部分时间线程处于等待IO操作完成的状态,CPU利用率不高。可以设置较大的corePoolSize,让CPU在等待IO的间隙去处理其他线程的任务,提高吞吐量。常见的建议是 CPU核心数 * 2,或者根据IO阻塞时间和CPU时间的比例(线程数 = CPU核心数 * (1 + 平均等待时间 / 平均计算时间))来估算。但这只是经验公式,最佳值需要通过性能测试来确定
  • 系统资源:服务器的CPU核心数、内存大小。
  • 并发需求:系统需要同时处理多少个任务。
  • 任务队列:队列类型和容量也会影响corePoolSize的选择。

经验法则:先根据理论(CPU/IO密集)估算,然后通过压力测试和监控(CPU利用率、任务平均等待时间、队列长度)进行调优。

默认allowCoreThreadTimeOutfalse,核心线程永不超时。如果设置为true,核心线程也会像非核心线程一样,在空闲时间超过keepAliveTime后被回收,直到线程数为0。这适用于对资源消耗非常敏感,且任务负载波动很大的场景。

  • 定义:线程池允许创建的最大线程数量。它限制了线程池的规模上限。
  • 包含关系maximumPoolSize >= corePoolSize。它包括了核心线程和非核心线程(临时工)。
  • 作用:为线程池在应对突发流量或任务积压时提供了额外的处理能力。当核心线程都在忙,并且任务队列也满了之后,线程池才会尝试创建新的线程(非核心线程),但总线程数不会超过maximumPoolSize

只有同时满足以下两个条件时,才会创建非核心线程:

  1. 当前运行的线程数达到了corePoolSize
  2. 任务队列(workQueue)已满。

maximumPoolSize的设置同样需要权衡:

  • 太小:限制了线程池处理峰值负载的能力,当核心线程和队列都满时,新任务会被拒绝。
  • 太大:可能导致系统资源(内存、线程句柄)耗尽,尤其是在使用CachedThreadPool时(其maximumPoolSizeInteger.MAX_VALUE)。创建过多线程也会增加上下文切换的开销。

设置依据

  • 峰值负载预估:系统在最高峰时可能需要处理多少并发任务。
  • 任务队列容量:如果队列很大甚至是无界的,maximumPoolSize的意义就不大了(除了SynchronousQueue),因为很难达到创建非核心线程的条件。
  • 系统承受能力:服务器能同时支持的最大线程数是多少?过多的线程不仅消耗内存,还可能触及操作系统的线程数限制。
  • 拒绝策略:如果希望尽量不拒绝任务,可以适当增大maximumPoolSize(配合有界队列),但要确保系统能承受。如果允许拒绝任务,则可以设置得相对保守。

重要提示:如果使用无界队列(如默认的LinkedBlockingQueue),maximumPoolSize参数实际上无效(因为队列永远不会满,也就永远不会触发创建非核心线程的条件)。这是Executors.newFixedThreadPoolnewSingleThreadExecutor的潜在风险之一。只有在使用有界队列或者SynchronousQueue时,maximumPoolSize才有意义。

4.3 keepAliveTime & unit:线程存活时间

Section titled “4.3 keepAliveTime & unit:线程存活时间”
  • keepAliveTime:当线程池中的线程数量大于corePoolSize时,多余的空闲线程(即非核心线程)在终止前可以存活的最长时间。
  • unitkeepAliveTime的时间单位,是java.util.concurrent.TimeUnit枚举类的值(如SECONDS, MILLISECONDS, MINUTES等)。
  • 默认情况下,keepAliveTime仅适用于非核心线程(即线程数超过corePoolSize的部分)。当一个非核心线程空闲时间达到keepAliveTime,它就会被回收。
  • 特殊情况:如果allowCoreThreadTimeOut被设置为true,那么keepAliveTime适用于核心线程。当线程池中的线程(包括核心线程)空闲时间达到keepAliveTime,它们都会被回收,直到线程数量缩减到0。
  • 值的大小
    • 较小的值:可以更快地回收空闲的非核心线程,节约资源,适用于任务负载波动大且峰值持续时间短的场景。
    • 较大的值(或0):让非核心线程存活更长时间,减少因任务峰值再次到来而重新创建线程的开销,适用于峰值可能频繁出现的场景。如果设置为0,表示空闲的非核心线程会立即被终止。
  • maximumPoolSize的配合keepAliveTime主要用于管理那些为了应对临时高峰而创建出来的非核心线程。设置一个合理的存活时间可以在资源利用和响应速度之间找到平衡点。

workQueueThreadPoolExecutor中用于暂存等待执行的任务的阻塞队列BlockingQueue)。它是连接任务提交者和线程池工作线程的桥梁,起着缓冲调度的关键作用。

当所有核心线程都在忙碌时,新提交的任务会首先尝试放入workQueue。只有当workQueue也满了(对于有界队列而言),线程池才会考虑创建非核心线程。

队列的选择对线程池的行为模式有着决定性的影响。

java.util.concurrent包提供了多种BlockingQueue实现,适用于ThreadPoolExecutor的主要有以下几种:

  1. ArrayBlockingQueue

    • 特点:基于数组实现的有界阻塞队列。必须在创建时指定容量。
    • 行为:按FIFO(先进先出)原则对元素进行排序。
    • 优点:实现简单,有界特性可以防止资源耗尽。
    • 缺点:容量固定,可能需要预估好容量;入队和出队操作可能需要加锁(读写共用一把锁,在高并发下性能可能受影响,但ThreadPoolExecutor通常只在一端操作密集)。
    • 示例new ArrayBlockingQueue<>(100) 创建容量为100的队列。
  2. LinkedBlockingQueue

    • 特点:基于链表实现的可选有界/无界阻塞队列。
    • 行为:按FIFO排序。
    • 优点
      • 无界时(默认构造函数new LinkedBlockingQueue<>()):理论上可以接收无限任务(受限于内存),吞吐量通常高于ArrayBlockingQueue(读写锁分离)。
      • 有界时(带容量参数的构造函数new LinkedBlockingQueue<>(1000)):兼具链表特性和有界控制。
    • 缺点
      • 无界时:可能导致任务无限积压,最终耗尽内存(OOM风险),使得maximumPoolSizekeepAliveTime参数失效。Executors.newFixedThreadPoolnewSingleThreadExecutor使用的就是它!
      • 有界时:需要指定容量。
    • 示例new LinkedBlockingQueue<>() (无界), new LinkedBlockingQueue<>(1000) (有界,容量1000)。
  3. SynchronousQueue

    • 特点:一个不存储元素的阻塞队列。每个插入操作必须等待一个相应的移除操作,反之亦然。可以看作是一个“直接传递”或“握手”通道。
    • 行为:提交的任务不会被实际存储,而是直接尝试交给一个正在等待任务的线程。如果没有空闲线程可用,offer()方法会返回false
    • 优点:传递效率高,没有缓冲延迟。
    • 缺点:由于不存储任务,它通常需要配合**较大或无限制的maximumPoolSize**使用,否则任务很容易被拒绝。
    • 应用Executors.newCachedThreadPool使用的就是它,配合Integer.MAX_VALUEmaximumPoolSize,使得线程池能够快速创建新线程来处理任务,但有OOM风险。适用于处理大量短生命周期的任务。
    • 示例new SynchronousQueue<>()
  4. PriorityBlockingQueue

    • 特点:一个支持优先级排序的无界阻塞队列。
    • 行为:存入队列的任务必须实现Comparable接口,或者在创建队列时提供Comparator。队列会根据任务的优先级决定出队顺序(优先级高的先出)。
    • 优点:可以处理具有不同优先级的任务。
    • 缺点
      • 无界队列,同样存在OOM风险。
      • 优先级判断和维护有一定开销。
    • 示例new PriorityBlockingQueue<>()
  5. DelayQueue

    • 特点:一个支持延时获取元素的无界阻塞队列。
    • 行为:存入队列的任务必须实现Delayed接口。只有当任务的延迟时间到了,才能从队列中取出。
    • 优点:适用于实现定时任务、缓存过期处理等场景。
    • 应用ScheduledThreadPoolExecutor内部使用了它。
    • 缺点:无界队列,有OOM风险。
    • 示例new DelayQueue<>()

不同队列对线程池行为的影响 (场景分析)

Section titled “不同队列对线程池行为的影响 (场景分析)”
  • 使用有界队列(ArrayBlockingQueue, LinkedBlockingQueue(capacity)

    • 流程:核心线程满 -> 入队 -> 队列满 -> 创建非核心线程 -> 达到最大线程数 -> 触发拒绝策略。
    • 优点:资源可控,能有效防止OOM。可以通过调整队列容量和maximumPoolSize来平衡吞吐量和资源消耗,提供一定的“反压”能力。
    • 缺点:队列容量设置不当可能导致任务处理不及时或频繁拒绝任务。
    • 适用场景:需要精确控制资源消耗、任务量相对可预测、系统稳定性要求高的场景。这是生产环境中最推荐的选择。
  • 使用无界队列(LinkedBlockingQueue(), PriorityBlockingQueue, DelayQueue

    • 流程:核心线程满 -> 无限入队。
    • 行为maximumPoolSizekeepAliveTime参数失效(因为队列永远不会满),线程数最多只会增长到corePoolSize不会触发拒绝策略(除非系统崩溃)。
    • 优点:能处理任意数量的任务(只要内存够),实现简单。
    • 缺点极易导致OOM!如果任务生产速度持续大于消费速度,内存会被耗尽。
    • 适用场景:任务执行非常快,或者任务量确定不会耗尽内存,且希望简化配置的场景。但强烈不推荐在生产环境中使用,除非有充分的理由和监控。
  • 使用SynchronousQueue

    • 流程:尝试直接交给空闲线程 -> 没有空闲线程,且线程数 < maximumPoolSize -> 创建新线程 -> 线程数达到maximumPoolSize -> 触发拒绝策略。
    • 行为:线程池倾向于快速创建新线程而不是排队。如果maximumPoolSize很大(如CachedThreadPoolInteger.MAX_VALUE),会创建大量线程。
    • 优点:任务提交和执行之间的延迟最小。
    • 缺点:如果maximumPoolSize无限制,可能创建过多线程导致OOM或系统崩溃。如果maximumPoolSize有限,则拒绝任务的阈值较低。
    • 适用场景:处理大量、执行时间非常短、需要快速响应的任务,且系统资源充足或maximumPoolSize设置合理。

选择建议
在绝大多数生产场景下,优先考虑使用有界队列 (ArrayBlockingQueueLinkedBlockingQueue 带容量)。容量大小需要根据任务特性、处理能力和可接受的延迟进行压测和调优。

  • 定义:一个用于创建新线程的对象。ThreadPoolExecutor在需要创建新线程(无论是核心还是非核心)时,会调用threadFactorynewThread(Runnable r)方法。
  • 默认实现Executors.defaultThreadFactory()。它创建的线程都是非守护线程daemon=false)、具有标准优先级NORM_PRIORITY),并且线程名字是pool-N-thread-M的形式(N是线程池序号,M是线程序号)。

为什么需要自定义ThreadFactory

  1. 线程命名:默认的线程名 pool-N-thread-M 辨识度不高。当系统出现问题(如死锁、性能瓶颈)需要分析线程堆栈(Thread Dump)时,有意义的线程名(如 order-service-pool-%d, payment-notify-thread-%d)能让你快速定位问题属于哪个业务或哪个线程池。
  2. 设置守护线程(Daemon Thread):如果希望线程池中的线程不阻止JVM退出(例如,用于执行一些后台辅助任务),可以将线程设置为守护线程(thread.setDaemon(true))。
  3. 设置线程优先级:虽然不推荐频繁调整线程优先级,但在特定场景下可能需要(thread.setPriority(Thread.MAX_PRIORITY))。
  4. 设置未捕获异常处理器:可以为线程池创建的每个线程设置一个统一的UncaughtExceptionHandler,用于记录或处理那些未在任务代码中捕获的异常。这对于问题排查非常重要。
  5. 线程组(ThreadGroup):可以将线程池创建的线程归属到特定的ThreadGroup中,便于管理(虽然ThreadGroup在现代Java中用得较少)。

实现java.util.concurrent.ThreadFactory接口即可:

import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
public class NamedThreadFactory implements ThreadFactory {
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
private final boolean daemon;
private final int priority;
private final Thread.UncaughtExceptionHandler uncaughtExceptionHandler;
public NamedThreadFactory(String poolName) {
this(poolName, false, Thread.NORM_PRIORITY, null);
}
public NamedThreadFactory(String poolName, boolean daemon, int priority, Thread.UncaughtExceptionHandler handler) {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
namePrefix = poolName + "-thread-";
this.daemon = daemon;
this.priority = priority;
this.uncaughtExceptionHandler = handler;
}
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);
// 设置守护状态
if (t.isDaemon() != daemon) {
t.setDaemon(daemon);
}
// 设置优先级
if (t.getPriority() != priority) {
t.setPriority(priority);
}
// 设置未捕获异常处理器
if (uncaughtExceptionHandler != null) {
t.setUncaughtExceptionHandler(uncaughtExceptionHandler);
} else {
// 默认给一个简单的打印处理器
t.setUncaughtExceptionHandler((thread, e) ->
System.err.println("Uncaught exception in thread '" + thread.getName() + "': " + e.getMessage())
);
}
System.out.println("创建线程: " + t.getName() + ", Daemon: " + t.isDaemon() + ", Priority: " + t.getPriority());
return t;
}
// 使用示例
public static void main(String[] args) {
ThreadFactory factory = new NamedThreadFactory(
"my-business-pool",
false, // 非守护线程
Thread.NORM_PRIORITY, // 普通优先级
(thread, throwable) -> System.err.println("线程 " + thread.getName() + " 抛出未捕获异常: " + throwable)
);
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, 4, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10),
factory, // 使用自定义工厂
new ThreadPoolExecutor.AbortPolicy()
);
// 提交一个会抛异常的任务
executor.execute(() -> {
System.out.println(Thread.currentThread().getName() + " is running...");
throw new RuntimeException("任务执行出错!");
});
executor.shutdown();
}
}

使用第三方库(如Guava)简化创建

Section titled “使用第三方库(如Guava)简化创建”

Google的Guava库提供了ThreadFactoryBuilder,可以更方便地创建自定义ThreadFactory

// 需要引入 Guava 依赖
// import com.google.common.util.concurrent.ThreadFactoryBuilder;
ThreadFactory customThreadFactory = new ThreadFactoryBuilder()
.setNameFormat("order-process-pool-%d") // 设置线程名称格式
.setDaemon(false) // 设置为非守护线程
.setPriority(Thread.NORM_PRIORITY) // 设置优先级
.setUncaughtExceptionHandler((thread, e) -> log.error("线程池 {} 发生未捕获异常", thread.getName(), e)) // 设置异常处理器
.build();
ThreadPoolExecutor executor = new ThreadPoolExecutor(
// ... 其他参数 ...
customThreadFactory,
// ... 拒绝策略 ...
);

强烈建议:在生产项目中,总是使用自定义的ThreadFactory,至少要做到有意义的线程命名设置未捕获异常处理器

4.6 rejectedExecutionHandler:拒绝策略

Section titled “4.6 rejectedExecutionHandler:拒绝策略”

当线程池同时满足以下两个条件时,新提交的任务会被拒绝:

  1. 线程池中的线程数量已经达到了maximumPoolSize
  2. 工作队列(workQueue)也已经满了。

此时,线程池无法再接收新的任务,必须按照预设的拒绝策略来处理这个新任务。

ThreadPoolExecutor提供了四种开箱即用的拒绝策略(它们都是ThreadPoolExecutor的静态内部类):

  1. AbortPolicy(中止策略 - 默认)

    • 行为:直接抛出RejectedExecutionException(运行时异常)。调用者需要捕获并处理这个异常。

    • 源码

      public static class AbortPolicy implements RejectedExecutionHandler {
      public AbortPolicy() { }
      public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
      // 直接抛出异常
      throw new RejectedExecutionException("Task " + r.toString() +
      " rejected from " + e.toString());
      }
      }
      + 1
      + 2
      + 3
      + 4
      + 5
      + 6
      + 7
      + 8
    • 优点:能够清晰地通知调用方任务被拒绝了,调用方可以根据业务需要进行处理(重试、降级、记录日志等)。

    • 缺点:如果调用方没有处理异常,程序可能会中断。

    • 适用场景:需要明确知道任务失败的情况,且任务比较重要,不能随意丢弃,希望调用方介入处理的场景。这是最常用的默认策略。

  2. CallerRunsPolicy(调用者运行策略)

    • 行为:既不抛弃任务,也不抛出异常,而是在提交任务的线程(调用executesubmit的线程)中直接执行这个被拒绝的任务

    • 源码

      public static class CallerRunsPolicy implements RejectedExecutionHandler {
      public CallerRunsPolicy() { }
      public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
      // 检查线程池是否已关闭,未关闭则在调用者线程运行任务
      if (!e.isShutdown()) {
      r.run(); // 注意:这里是直接调用run(),不是启动新线程
      }
      }
      }
      + 1
      + 2
      + 3
      + 4
      + 5
      + 6
      + 7
      + 8
      + 9
    • 优点

      • 确保任务不会丢失(除非线程池已关闭)。
      • 提供了一种自然的“反压”(Back Pressure)机制。当线程池饱和时,提交任务的线程会被占用去执行任务,从而无法快速提交更多的新任务,间接降低了任务提交的速率,有助于系统负载趋于平稳。
    • 缺点:任务执行会阻塞提交任务的线程,如果提交任务的线程很重要(例如处理网络请求的IO线程),可能会影响其主流程的响应性能。

    • 适用场景:希望系统在高负载时能够平滑降级,任务不丢失,且调用者线程可以承受执行任务的开销。适用于需要保证任务最终执行,但可以接受一定延迟的场景。

  3. DiscardPolicy(丢弃策略)

    • 行为静默地丢弃被拒绝的任务,不做任何处理,也不抛出异常。就像任务从未提交过一样。

    • 源码

      public static class DiscardPolicy implements RejectedExecutionHandler {
      public DiscardPolicy() { }
      public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
      // 什么都不做
      }
      }
      + 1
      + 2
      + 3
      + 4
      + 5
      + 6
    • 优点:实现简单,对调用方无影响。

    • 缺点任务会丢失!并且没有任何通知。如果任务很重要,使用此策略可能导致数据丢失或业务逻辑不完整。

    • 适用场景:任务不重要,允许丢失。例如,一些非关键的日志记录、统计信息上报等,丢失一两条影响不大。谨慎使用!

  4. DiscardOldestPolicy(丢弃最旧策略)

    • 行为:丢弃工作队列队头(即等待时间最长)的任务,然后尝试重新提交当前被拒绝的任务。如果再次提交失败(例如,在执行丢弃和重试之间线程池关闭了),任务仍然会被丢弃。

    • 源码

      public static class DiscardOldestPolicy implements RejectedExecutionHandler {
      public DiscardOldestPolicy() { }
      public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
      if (!e.isShutdown()) {
      // 丢弃队列头部的任务
      e.getQueue().poll();
      // 再次尝试提交当前任务
      e.execute(r);
      }
      }
      }
      + 1
      + 2
      + 3
      + 4
      + 5
      + 6
      + 7
      + 8
      + 9
      + 10
      + 11
    • 优点:尝试腾出空间来接纳新任务,优先保证新任务的执行。

    • 缺点:会丢失队列中最旧的任务,同样存在任务丢失风险。如果队列使用的是PriorityBlockingQueue,那么丢弃的是优先级最低的任务。

    • 适用场景:希望尽可能处理新任务,旧任务可以容忍丢失的场景。例如,发布-订阅模型中,处理最新的消息比处理旧消息更重要。同样需要谨慎使用。

如果标准策略无法满足需求,可以实现java.util.concurrent.RejectedExecutionHandler接口来创建自定义策略。

import java.util.concurrent.*;
// 自定义策略:记录被拒绝的任务日志,然后丢弃
class LoggingDiscardPolicy implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 获取任务信息(如果任务是特定类型,可以获取更多信息)
String taskInfo = r.toString();
System.err.println("警告:任务被拒绝!任务信息: " + taskInfo + ", 线程池状态: " + executor);
// 这里可以加入更详细的日志记录逻辑,比如记录到文件或发送告警
// 然后执行丢弃逻辑(或者其他逻辑,如放入备用队列、数据库等)
// 此处仅做打印,行为类似DiscardPolicy
}
}
// 自定义策略:尝试等待一段时间,如果队列仍满则抛异常
class WaitAndAbortPolicy implements RejectedExecutionHandler {
private final long waitMillis;
public WaitAndAbortPolicy(long waitMillis) {
this.waitMillis = waitMillis;
}
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
if (!executor.isShutdown()) {
try {
// 尝试在队列上阻塞等待指定时间
if (executor.getQueue().offer(r, waitMillis, TimeUnit.MILLISECONDS)) {
// 成功放入队列
System.out.println("任务 " + r.toString() + " 等待后成功入队。");
return;
}
} catch (InterruptedException e) {
// 等待期间被中断,恢复中断状态
Thread.currentThread().interrupt();
}
}
// 等待超时或线程池关闭或被中断,最终还是拒绝并抛异常
throw new RejectedExecutionException("任务 " + r.toString() + " 在等待 " + waitMillis + "ms 后仍然被拒绝。");
}
}
public class CustomRejectionDemo {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
1, 1, 0L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1), // 核心1,队列1,最大1
Executors.defaultThreadFactory(),
// new LoggingDiscardPolicy() // 使用记录日志策略
new WaitAndAbortPolicy(1000) // 使用等待策略
);
// 提交3个任务,肯定会触发拒绝
for (int i = 0; i < 3; i++) {
final int taskId = i;
try {
executor.execute(() -> {
System.out.println("执行任务 " + taskId);
try {
Thread.sleep(2000); // 模拟耗时
} catch (InterruptedException e) { Thread.currentThread().interrupt(); }
});
System.out.println("任务 " + taskId + " 已提交");
} catch (RejectedExecutionException e) {
System.err.println("捕获到拒绝异常: " + e.getMessage());
}
}
executor.shutdown();
}
}

自定义策略可以实现非常灵活的逻辑,例如:

  • 将拒绝的任务持久化到数据库或消息队列,以便后续重试。
  • 发送监控告警。
  • 根据任务类型采取不同的拒绝方式。
  • 实现更复杂的反压机制。

特殊场景:自定义策略强制重试?

Section titled “特殊场景:自定义策略强制重试?”

如你提供的文档中提到的,有时可能会尝试实现一个“强制重试”的策略,例如使用put()方法强制将任务放入队列:

// 极不推荐的策略!
public static class ForcePutPolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
try {
// 强制将任务放入队列,如果队列满,会阻塞提交线程!
e.getQueue().put(r);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RejectedExecutionException("强制入队时被中断", ie);
}
}
}
}

这种策略会发生什么?

  • 阻塞提交线程put()方法在队列满时会阻塞,直到队列有空间。这意味着提交任务的线程会被阻塞,无法继续执行。
  • 掩盖问题:它掩盖了线程池处理不过来的根本问题,只是将压力转移到了提交任务的线程上。
  • 潜在死锁:如果线程池中的任务依赖于提交线程继续执行某些操作才能完成(虽然不常见),可能会导致死锁。
  • 内存压力不减:任务最终还是积压在队列中,内存压力依然存在,并没有解决资源瓶颈。
  • 响应延迟:提交线程被阻塞,导致其负责的其他工作(如响应用户请求)延迟。

实际生产中的应用场景?

虽然极其不推荐,但在某些极端场景下,如果满足以下所有条件,可能会考虑(但仍需谨慎评估风险):

  1. 任务绝对不能丢失:业务要求100%的任务都必须被处理,宁可阻塞提交端,也不能丢失任务。
  2. 提交端可以容忍阻塞:提交任务的线程不是关键路径上的线程(例如,不是处理实时请求的Web线程,而是一个后台批处理任务的提交线程),可以接受阻塞等待。
  3. 系统资源有保障:有足够的内存来支撑队列的增长,且有监控机制防止内存溢出。
  4. 临时性过载:预期系统只是临时性过载,后续处理能力会跟上,队列不会无限增长。

更好的替代方案
通常,使用CallerRunsPolicy提供的反压机制,或者自定义策略将任务持久化到外部存储(如MQ、DB)进行异步处理,是更健壮、更推荐的方案。强制使用put()阻塞提交线程往往是最后的、效果不佳的选择。

5. 深入理解ThreadPoolExecutor工作原理

Section titled “5. 深入理解ThreadPoolExecutor工作原理”

了解了核心参数后,我们深入内部,看看ThreadPoolExecutor是如何协调这些组件运作的。

当一个新任务通过execute()方法提交给ThreadPoolExecutor时,它会经历以下决策流程(这里以图解文字描述):

开始: 提交新任务task

当前运行线程数 < corePoolSize?

创建新 **核心线程** 执行任务

任务队列 workQueue 是否已满?

将任务添加到 workQueue

当前运行线程数 < maximumPoolSize?

创建新 **非核心线程** 执行任务

执行 **拒绝策略** rejectedExecutionHandler

结束

流程解读

  1. 判断核心线程数:检查当前活动的线程数是否小于corePoolSize
    • 如果是,直接创建新的核心线程来执行任务(即使有空闲的核心线程)。结束。
    • 如果否,进入下一步。
  2. 判断队列是否已满:尝试将任务添加到workQueue中(使用offer()方法,非阻塞)。
    • 如果添加成功(队列未满),任务入队等待执行。结束。(但会进行二次检查,如果此时线程池状态改变或线程数为0,会确保有线程处理队列任务)。
    • 如果添加失败(队列已满),进入下一步。
  3. 判断最大线程数:检查当前活动的线程数是否小于maximumPoolSize
    • 如果是,创建新的非核心线程来执行任务。结束。
    • 如果否(线程数已达最大值),进入下一步。
  4. 执行拒绝策略:调用配置的RejectedExecutionHandler来处理这个无法接收的任务。结束。

这个流程清晰地体现了ThreadPoolExecutor处理任务的优先级:核心线程 > 任务队列 > 非核心线程 > 拒绝策略

5.2 任务调度优先级:核心 -> 队列 -> 最大

Section titled “5.2 任务调度优先级:核心 -> 队列 -> 最大”

这个优先级顺序是ThreadPoolExecutor设计的关键,它旨在:

  • 优先利用核心线程:保证基本的处理能力,快速响应。
  • 利用队列进行缓冲:当核心线程忙不过来时,用队列作为缓冲,避免立即创建更多线程,节约资源。
  • 利用非核心线程应对峰值:只有在核心线程和队列都无法处理时,才创建非核心线程来应对突发流量。
  • 拒绝策略作为最后防线:在资源真正耗尽时,通过拒绝策略保护系统不被压垮。

ThreadPoolExecutor内部通过一个名为Worker的内部类来管理工作线程。Worker类本身既实现了Runnable接口,又继承了AbstractQueuedSynchronizer(AQS)。

private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
// 关联的实际工作线程
final Thread thread;
// 该Worker创建时分配的第一个任务,可能为null
Runnable firstTask;
// 用于统计该Worker完成了多少任务(易变)
volatile long completedTasks;
Worker(Runnable firstTask) {
setState(-1); // 初始化AQS状态为-1,防止在runWorker之前被中断
this.firstTask = firstTask;
// 通过ThreadFactory创建实际的线程,并将当前Worker(Runnable)作为任务传递
this.thread = getThreadFactory().newThread(this);
}
// 当thread.start()被调用时,会执行这个run方法
public void run() {
runWorker(this); // 调用外部类的runWorker执行核心逻辑
}
// AQS相关方法,实现了一个简单的不可重入锁,用于控制中断
// ... lock(), unlock(), isLocked() ...
}

Worker线程的生命周期大致如下:

  • ThreadPoolExecutor需要创建新线程时(通过addWorker()方法),会实例化一个Worker对象。
  • Worker的构造函数中,会调用ThreadFactory来创建一个新的Thread,并将Worker自身(它实现了Runnable)作为Thread的目标任务。
  • 然后,调用worker.thread.start()方法启动这个新创建的线程。
  • 线程启动后,执行Workerrun()方法,该方法随即调用外部ThreadPoolExecutorrunWorker(this)方法。
  • runWorker是工作线程的核心逻辑所在。它在一个循环中不断地尝试从任务队列中获取任务并执行。
// ThreadPoolExecutor.runWorker() 简化逻辑
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask; // 获取初始任务
w.firstTask = null;
w.unlock(); // 允许中断 (构造时state=-1, unlock后变为0)
boolean completedAbruptly = true; // 标记是否异常退出
try {
// 循环:只要能获取到任务,就一直执行
while (task != null || (task = getTask()) != null) {
w.lock(); // 加锁,表示正在执行任务,防止shutdownNow中断正在运行的任务
// 如果线程池停止,或者线程被中断(且线程池已停止),确保线程被中断
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() && runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task); // 钩子方法:任务执行前
Throwable thrown = null;
try {
task.run(); // **真正执行任务**
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x); // 包装成Error
} finally {
afterExecute(task, thrown); // 钩子方法:任务执行后
}
} finally {
task = null; // 任务执行完毕,清空引用
w.completedTasks++; // 完成任务计数增加
w.unlock(); // 解锁
}
}
completedAbruptly = false; // 正常退出循环
} finally {
processWorkerExit(w, completedAbruptly); // 处理工作线程退出
}
}
  • runWorker循环中的getTask()方法负责从工作队列workQueue中获取下一个要执行的任务。
  • getTask()是一个阻塞方法,它会考虑线程池状态、核心线程超时设置、队列情况等。
// ThreadPoolExecutor.getTask() 简化逻辑
private Runnable getTask() {
boolean timedOut = false; // 上次获取任务是否超时
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// 条件1: 检查线程池是否应该终止?
// 如果状态 >= SHUTDOWN 且 (状态 >= STOP 或 队列为空)
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount(); // 原子减少工作线程计数
return null; // 返回null,导致runWorker循环退出
}
int wc = workerCountOf(c);
// 条件2: 是否允许线程超时回收?
// allowCoreThreadTimeOut为true 或者 当前线程数 > corePoolSize
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
// 条件3: 是否应该减少线程数?
// (线程数 > maximumPoolSize 或 (允许超时且上次已超时)) 且 (线程数 > 1 或 队列为空)
// 防止maximumPoolSize动态调小后无法回收;防止在空队列时无限等待
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c)) // CAS减少计数
return null; // 返回null,退出循环
continue; // CAS失败,重试
}
// 条件4: 从队列获取任务
try {
// 如果允许超时(timed=true),使用poll带超时获取
// 否则,使用take阻塞获取
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r; // 获取到任务,返回
timedOut = true; // 未获取到任务(超时或poll返回null),标记超时
} catch (InterruptedException retry) {
// 如果被中断,重置超时标记并重试(可能因为shutdown)
timedOut = false;
}
}
}

getTask()的逻辑确保了:

  • 在线程池关闭时,工作线程能正确退出。
  • 非核心线程(或允许超时的核心线程)在空闲超时后能退出。
  • 能响应maximumPoolSize的动态调整。
  • 通过poll(带超时)或take(阻塞)从队列获取任务。

Worker线程(即runWorker循环)在以下情况下会退出:

  1. getTask()返回null
    • 线程池状态变为SHUTDOWN且队列为空。
    • 线程池状态变为STOP
    • 线程是多余的(超过maximumPoolSize或空闲超时)且成功减少了workerCount
  2. 任务执行过程中抛出了未捕获的异常(completedAbruptly = true)。

runWorker循环退出时(无论是正常退出还是异常退出),都会执行finally块中的processWorkerExit(w, completedAbruptly)方法。

processWorkerExit负责:

  • 将当前Worker从线程池的workers集合中移除。
  • 更新已完成任务的总数。
  • 关键:如果线程是异常退出的,为了维持线程池的处理能力,它会尝试创建一个新的Worker来替代(除非线程池正在停止)。
  • 尝试调用tryTerminate(),检查是否满足线程池终止的条件。

这个精巧的生命周期管理机制,保证了线程池能够动态维护其工作线程数量,处理异常退出,并在关闭时有序地终止所有线程。

ThreadPoolExecutor内部维护了五种状态,用于控制任务的接收、处理和线程池的生命周期。

状态定义:RUNNING, SHUTDOWN, STOP, TIDYING, TERMINATED

Section titled “状态定义:RUNNING, SHUTDOWN, STOP, TIDYING, TERMINATED”

这些状态定义在ThreadPoolExecutor类的开头:

// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS; // -536870912 (二进制: 111 000...000)
private static final int SHUTDOWN = 0 << COUNT_BITS; // 0 (二进制: 000 000...000)
private static final int STOP = 1 << COUNT_BITS; // 536870912 (二进制: 001 000...000)
private static final int TIDYING = 2 << COUNT_BITS; // 1073741824 (二进制: 010 000...000)
private static final int TERMINATED = 3 << COUNT_BITS; // 1610612736 (二进制: 011 000...000)
// COUNT_BITS = Integer.SIZE - 3 = 29
// CAPACITY = (1 << COUNT_BITS) - 1 = 536870911 (低29位能表示的最大worker数)
  • RUNNING (-1):
    • 状态描述:线程池正常运行
    • 行为:接受新任务提交,处理队列中的任务。
    • 初始状态:线程池创建后的默认状态。
  • SHUTDOWN (0):
    • 状态描述:线程池正在关闭(优雅关闭)。
    • 行为:不接受新任务提交,但会继续处理队列中已存在的任务。
    • 触发条件:调用shutdown()方法。
  • STOP (1):
    • 状态描述:线程池立即停止
    • 行为:不接受新任务提交,不处理队列中的任务,并且尝试中断正在执行的任务。
    • 触发条件:调用shutdownNow()方法。
  • TIDYING (2):
    • 状态描述:线程池整理中。所有任务都已终止,工作线程数量为0。
    • 行为:即将执行terminated()钩子方法。这是一个中间状态。
    • 触发条件:
      • 当线程池处于SHUTDOWN状态,并且工作队列和线程池(workers集合)都为空时。
      • 当线程池处于STOP状态,并且线程池(workers集合)为空时。
  • TERMINATED (3):
    • 状态描述:线程池已终止
    • 行为:terminated()钩子方法已执行完毕。线程池彻底结束。
    • 触发条件:TIDYING状态下,terminated()方法执行完成。

注意这些状态的值是单调递增的(RUNNING < SHUTDOWN < STOP < TIDYING < TERMINATED),这使得状态比较非常方便(例如 runState >= SHUTDOWN)。

ThreadPoolExecutor使用一个AtomicInteger类型的变量ctl同时存储两个信息:

  1. 线程池的运行状态 (runState):存储在ctl的高3位。
  2. 当前活动的工作线程数量 (workerCount):存储在ctl的低29位。
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// COUNT_BITS决定了workerCount占用的位数
private static final int COUNT_BITS = Integer.SIZE - 3; // 32 - 3 = 29
// CAPACITY是workerCount的最大值 (低29位全为1)
private static final int CAPACITY = (1 << COUNT_BITS) - 1; // 000111...111 (共29个1)
// Helper方法用于打包和解包ctl
private static int runStateOf(int c) { return c & ~CAPACITY; } // 取高3位 (与上 111000...000)
private static int workerCountOf(int c) { return c & CAPACITY; } // 取低29位 (与上 000111...111)
private static int ctlOf(int rs, int wc) { return rs | wc; } // 将状态和数量合并

为什么这样设计?

  • 原子性:将两个关键变量(状态和线程数)打包到一个AtomicInteger中,使得可以通过一次CAS操作同时更新或检查这两个值,避免了使用两个独立变量可能需要的锁或其他同步机制,提高了并发性能。例如,在addWorker中需要同时检查状态和增加线程数。
  • 空间效率:用一个整型变量存储了两个信息。

线程池的状态转换是单向的,不可逆:

graph LR
RUNNING -- shutdown() --> SHUTDOWN;
RUNNING -- shutdownNow() --> STOP;
SHUTDOWN -- 队列和池都为空 --> TIDYING;
STOP -- 池为空 --> TIDYING;
TIDYING -- terminated()执行完毕 --> TERMINATED;
style RUNNING fill:#ccf
style SHUTDOWN fill:#cfc
style STOP fill:#fcc
style TIDYING fill:#f96
style TERMINATED fill:#ccc

转换路径

  • RUNNING -> SHUTDOWN (调用 shutdown())
  • RUNNING -> STOP (调用 shutdownNow())
  • SHUTDOWN -> TIDYING (当队列为空且worker数量为0时,在tryTerminate()中触发)
  • STOP -> TIDYING (当worker数量为0时,在tryTerminate()中触发)
  • TIDYING -> TERMINATED (当terminated()钩子方法执行完毕后,在tryTerminate()中触发)

理解这些状态及其转换对于正确使用线程池的关闭方法(shutdown(), shutdownNow(), awaitTermination())以及实现terminated()钩子方法至关重要。

6. 源码剖析:揭开ThreadPoolExecutor的面纱

Section titled “6. 源码剖析:揭开ThreadPoolExecutor的面纱”

通过阅读关键方法的源码,我们可以更深入地理解ThreadPoolExecutor的内部机制。以下源码基于OpenJDK,并添加了中文注释。

6.1 execute(Runnable command) 源码解读

Section titled “6.1 execute(Runnable command) 源码解读”

这是向线程池提交任务的最核心方法。

public void execute(Runnable command) {
if (command == null)
throw new NullPointerException(); // 任务不能为空
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would occur
* if checking first then creating the thread.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
int c = ctl.get(); // 获取包含状态和线程数的原子变量ctl
// 步骤 1: 检查核心线程数是否已满
if (workerCountOf(c) < corePoolSize) {
// 尝试添加一个新的Worker(核心线程),并将当前任务作为其第一个任务
// addWorker内部会原子性地检查状态和增加workerCount
if (addWorker(command, true)) // true表示尝试添加核心线程
return; // 添加成功,直接返回
c = ctl.get(); // 如果addWorker失败(可能因为并发修改),重新获取ctl
}
// 步骤 2: 尝试将任务加入工作队列
// 检查线程池是否处于RUNNING状态,并且尝试将任务放入队列(offer是非阻塞的)
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get(); // 重新获取ctl进行二次检查
// 检查1: 如果线程池状态不再是RUNNING(比如刚被shutdown),需要移除刚入队的任务
if (! isRunning(recheck) && remove(command)) // remove尝试从队列移除任务
reject(command); // 移除成功,执行拒绝策略
// 检查2: 如果线程池仍在运行,但工作线程数为0(可能之前的线程都异常退出了)
// 需要确保至少有一个线程来处理队列中的任务(即使是刚入队的这个)
else if (workerCountOf(recheck) == 0)
addWorker(null, false); // 添加一个非核心线程,但不分配初始任务(null)
// false表示非核心(但如果corePoolSize>0,可能实际创建的是核心)
// 主要是为了启动一个线程去消费队列
// 如果二次检查都通过,任务已在队列中,等待执行,方法返回
}
// 步骤 3: 如果队列已满,尝试创建非核心线程
else if (!addWorker(command, false)) // false表示尝试添加非核心线程
// 如果添加非核心线程也失败(通常意味着线程数已达maximumPoolSize)
reject(command); // 执行拒绝策略
}
// reject方法,调用配置的RejectedExecutionHandler
final void reject(Runnable command) {
handler.rejectedExecution(command, this);
}

源码要点

  • 完美体现了 核心线程 -> 队列 -> 非核心线程 -> 拒绝策略 的优先级。
  • 使用ctl原子变量进行状态和线程数的联合检查与更新。
  • 二次检查(Double-Check):在任务成功入队后,进行recheck是为了处理并发场景下的状态变化,确保任务不会在线程池关闭后仍留在队列中,或者在没有工作线程时任务无法被处理。
  • addWorker()是实际创建和启动线程的核心方法(后面会分析)。
  • remove()尝试从队列中移除任务,如果任务正在被执行则可能失败。
  • reject()调用用户配置的拒绝策略处理器。

6.2 submit(...) 方法与 FutureTask 源码解读

Section titled “6.2 submit(...) 方法与 FutureTask 源码解读”

submit方法用于提交需要返回结果的任务(Callable)或不需要返回结果的任务(Runnable),并返回一个Future对象。它是在AbstractExecutorService中实现的,内部通常会调用execute

AbstractExecutorService.java
// submit(Callable<T> task)
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
// 1. 将Callable包装成RunnableFuture (通常是FutureTask)
RunnableFuture<T> ftask = newTaskFor(task);
// 2. 调用execute方法执行这个RunnableFuture
execute(ftask);
// 3. 返回Future对象
return ftask;
}
// submit(Runnable task, T result)
public <T> Future<T> submit(Runnable task, T result) {
if (task == null) throw new NullPointerException();
// 1. 将Runnable和预设结果包装成RunnableFuture
RunnableFuture<T> ftask = newTaskFor(task, result);
// 2. 调用execute执行
execute(ftask);
// 3. 返回Future
return ftask;
}
// submit(Runnable task) - 返回 Future<?>
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
// 1. 将Runnable包装成RunnableFuture<?> (结果类型为null)
RunnableFuture<?> ftask = newTaskFor(task, null);
// 2. 调用execute执行
execute(ftask);
// 3. 返回Future
return ftask;
}
// newTaskFor - 辅助方法,用于创建RunnableFuture实例
// 默认返回 FutureTask
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
return new FutureTask<T>(runnable, value);
}
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
return new FutureTask<T>(callable);
}

FutureTask的关键作用

FutureTask是一个非常巧妙的类,它同时实现了RunnableFuture接口。

  • 作为Runnable:它可以被ThreadPoolExecutorexecute方法接受并执行。它的run()方法会调用内部包装的Callablecall()方法或Runnablerun()方法。
  • 作为Future:它提供了获取任务结果(get())、检查任务状态(isDone(), isCancelled())、取消任务(cancel())等方法。

FutureTask.run() 源码核心逻辑(简化版)

public void run() {
// 检查状态,如果不是NEW,或者CAS设置runner失败,直接返回
if (state != NEW || !STATE.compareAndSet(this, NEW, RUNNING))
return;
try {
Callable<V> c = callable; // 获取内部的Callable或Runnable适配器
if (c != null && state == RUNNING) {
V result;
boolean ran;
try {
result = c.call(); // **调用实际的任务逻辑**
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
setException(ex); // **如果任务抛异常,捕获并保存**
}
if (ran)
set(result); // **如果正常完成,保存结果**
}
} finally {
// runner设为null,防止内存泄漏
runner = null;
// 如果状态是INTERRUPTING,等待中断完成
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
// 保存正常结果
protected void set(V v) {
if (STATE.compareAndSet(this, RUNNING, COMPLETING)) {
outcome = v; // 将结果保存在outcome字段
STATE.setRelease(this, NORMAL); // 设置最终状态为NORMAL
finishCompletion(); // 唤醒等待结果的线程(调用get()的线程)
}
}
// 保存异常结果
protected void setException(Throwable t) {
if (STATE.compareAndSet(this, RUNNING, COMPLETING)) {
outcome = t; // 将异常保存在outcome字段
STATE.setRelease(this, EXCEPTIONAL); // 设置最终状态为EXCEPTIONAL
finishCompletion(); // 唤醒等待结果的线程
}
}

submitexecute的主要区别总结

  1. 返回值submit返回Futureexecute无返回值。
  2. 异常处理
    • execute提交的任务,如果内部抛出未捕获异常,会传递给线程的UncaughtExceptionHandler
    • submit提交的任务,如果内部抛出异常,异常会被FutureTask捕获并存储起来。只有当调用Future.get()时,这个异常才会被包装成ExecutionException重新抛出。如果不调用get(),异常可能会被“吞掉”(需要确保有其他机制处理,如FutureTaskdone()方法)。
  3. 任务类型submit可以接受CallableRunnableexecute只能接受Runnable

6.3 addWorker(Runnable firstTask, boolean core) 源码解读

Section titled “6.3 addWorker(Runnable firstTask, boolean core) 源码解读”

这是创建和启动工作线程的核心内部方法。

private boolean addWorker(Runnable firstTask, boolean core) {
// 外层循环,用于在CAS失败或状态改变时重试
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c); // 获取当前运行状态
// 检查是否允许添加Worker
// 条件1: 状态检查
// 如果状态 >= SHUTDOWN,通常不允许添加
// 特例: 如果状态是SHUTDOWN,且firstTask为null(表示不带新任务,只想启动worker处理队列),
// 并且队列不为空,则允许添加。
// 如果状态 >= STOP,或者状态是SHUTDOWN但firstTask不为null或队列为空,则不允许添加。
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false; // 不允许添加,返回false
// 内层循环,用于CAS增加workerCount
for (;;) {
int wc = workerCountOf(c); // 获取当前worker数量
// 条件2: 容量检查
// 如果worker数量超过理论上限CAPACITY,或者超过了core/maximum限制
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false; // 超过容量,返回false
// 尝试原子性地增加workerCount
if (compareAndIncrementWorkerCount(c))
break retry; // CAS成功!跳出外层循环,准备创建Worker
// CAS失败,说明ctl被其他线程修改了
c = ctl.get(); // 重新读取ctl
// 检查状态是否发生变化,如果变了,回到外层循环重新检查状态
if (runStateOf(c) != rs)
continue retry;
// 状态没变,只是workerCount变了,继续内层循环尝试CAS
}
}
// ---- workerCount增加成功,开始创建和启动Worker ----
boolean workerStarted = false; // 标记线程是否成功启动
boolean workerAdded = false; // 标记Worker对象是否成功添加到workers集合
Worker w = null;
try {
// 1. 创建Worker对象(内部会通过ThreadFactory创建Thread)
w = new Worker(firstTask);
final Thread t = w.thread; // 获取关联的Thread对象
if (t != null) {
// 2. 加全局锁(mainLock),保护对workers集合的访问
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 再次检查线程池状态(获取锁后可能状态已改变)
int rs = runStateOf(ctl.get());
// 如果状态 < SHUTDOWN,或者状态是SHUTDOWN且firstTask为null(允许添加用于处理队列的worker)
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
// 检查线程是否已意外启动(ThreadFactory实现可能有问题)
if (t.isAlive())
throw new IllegalThreadStateException();
// 3. 将新Worker添加到workers集合(HashSet)
workers.add(w);
workerAdded = true;
// 更新线程池达到的最大线程数统计
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
}
} finally {
mainLock.unlock(); // 释放锁
}
// 4. 如果Worker成功添加到集合,启动线程
if (workerAdded) {
t.start(); // **启动线程,开始执行 Worker.run() -> runWorker()**
workerStarted = true;
}
}
} finally {
// 5. 如果线程启动失败(例如ThreadFactory返回null,或添加到workers失败)
if (! workerStarted)
addWorkerFailed(w); // 执行清理操作(从workers移除,减少workerCount)
}
return workerStarted; // 返回线程是否成功启动
}
// compareAndIncrementWorkerCount - CAS增加workerCount
private boolean compareAndIncrementWorkerCount(int expect) {
return ctl.compareAndSet(expect, expect + 1);
}
// addWorkerFailed - 添加Worker失败时的清理
private void addWorkerFailed(Worker w) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
if (w != null)
workers.remove(w); // 从集合移除
decrementWorkerCount(); // 原子减少workerCount
tryTerminate(); // 尝试终止线程池(如果条件满足)
} finally {
mainLock.unlock();
}
}

源码要点

  • 双重循环 + CAS:处理并发增加workerCount的经典模式,既保证原子性,又避免了全程加锁。retry标签的使用很关键。
  • 状态检查:严格检查线程池状态,确保只在允许的情况下添加Worker。特别处理了SHUTDOWN状态下添加用于清空队列的Worker的逻辑。
  • 容量检查:确保线程数不超过corePoolSizemaximumPoolSize限制。
  • 全局锁mainLock:用于保护对workers这个HashSet的并发访问。添加和移除Worker对象时需要持有锁。
  • 启动线程:在Worker对象成功添加到workers集合后,才调用t.start()
  • 失败处理addWorkerFailed确保在任何步骤失败时,都能回滚workerCount并将部分添加的Worker对象清理掉。
  • tryTerminate():在增减workerCount或移除Worker后,都会尝试检查线程池是否满足终止条件。

已在 5.3 Worker线程的生命周期 中结合注释进行了分析。runWorker是工作线程执行任务的核心循环。

已在 5.3 Worker线程的生命周期 中结合注释进行了分析。getTask负责从队列获取任务,并处理线程超时回收逻辑。

6.6 线程池关闭过程源码解读 (shutdown(), shutdownNow(), tryTerminate())

Section titled “6.6 线程池关闭过程源码解读 (shutdown(), shutdownNow(), tryTerminate())”

理解线程池如何关闭同样重要。

shutdown() - 优雅关闭

public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock(); // 加全局锁
try {
checkShutdownAccess(); // 检查安全权限
// 1. 将状态推进到SHUTDOWN (如果当前是RUNNING)
advanceRunState(SHUTDOWN);
// 2. 中断所有空闲的Worker线程
// Worker在getTask()的take/poll等待时被中断会醒来,
// 然后检查到状态是SHUTDOWN,就会返回null导致退出。
interruptIdleWorkers();
onShutdown(); // 钩子方法,留给子类扩展 (ScheduledThreadPoolExecutor用到)
} finally {
mainLock.unlock(); // 释放锁
}
// 3. 尝试终止线程池
// 如果此时队列已空且worker数为0,会直接进入TIDYING->TERMINATED
tryTerminate();
}
// advanceRunState - CAS更新状态到目标状态(如果当前状态更小)
private void advanceRunState(int targetState) {
for (;;) {
int c = ctl.get();
// 如果当前状态 >= 目标状态,或CAS设置失败,则跳过或重试
if (runStateAtLeast(c, targetState) ||
ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c))))
break;
}
}
// interruptIdleWorkers - 中断空闲线程
// "空闲"指没有被lock()保护的线程,即不在执行任务代码,可能在getTask()阻塞
private void interruptIdleWorkers(boolean onlyOne) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 遍历所有Worker
for (Worker w : workers) {
Thread t = w.thread;
// 尝试获取Worker的锁(tryLock),如果成功表示它当前空闲(未执行任务)
// 并且线程未被中断
if (!t.isInterrupted() && w.tryLock()) {
try {
t.interrupt(); // 中断这个空闲线程
} catch (SecurityException ignore) {
} finally {
w.unlock(); // 释放锁
}
}
// 如果只中断一个,完成即退出
if (onlyOne)
break;
}
} finally {
mainLock.unlock();
}
}
// 重载版本,中断所有空闲线程
private void interruptIdleWorkers() {
interruptIdleWorkers(false);
}

shutdownNow() - 立即关闭

public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock(); // 加全局锁
try {
checkShutdownAccess();
// 1. 将状态推进到STOP
advanceRunState(STOP);
// 2. 中断所有Worker线程(无论是否空闲)
interruptWorkers();
// 3. 排空工作队列,将未执行的任务收集起来
tasks = drainQueue();
} finally {
mainLock.unlock(); // 释放锁
}
// 4. 尝试终止线程池
tryTerminate();
return tasks; // 返回未执行的任务列表
}
// interruptWorkers - 中断所有线程
private void interruptWorkers() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 直接中断集合中所有Worker对应的线程
for (Worker w : workers)
w.interruptIfStarted(); // 内部会检查线程是否启动且未中断
} finally {
mainLock.unlock();
}
}
// drainQueue - 排空队列
private List<Runnable> drainQueue() {
BlockingQueue<Runnable> q = workQueue;
ArrayList<Runnable> taskList = new ArrayList<Runnable>();
// 将队列元素转移到taskList
q.drainTo(taskList);
// 如果队列不支持drainTo,或者转移后队列仍不为空(并发?),手动poll
if (!q.isEmpty()) {
for (Runnable r : q.toArray(new Runnable[0])) {
if (q.remove(r)) // 尝试移除,成功则加入列表
taskList.add(r);
}
}
return taskList;
}

tryTerminate() - 尝试终止

这是shutdown(), shutdownNow(), processWorkerExit(), addWorkerFailed() 等方法都会调用的核心终止逻辑。

final void tryTerminate() {
for (;;) {
int c = ctl.get();
// 条件1: 如果线程池还在RUNNING,或者已经TIDYING/TERMINATED,
// 或者处于SHUTDOWN但队列不为空,则不能终止,直接返回。
if (isRunning(c) ||
runStateAtLeast(c, TIDYING) ||
(runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
return;
// 条件2: 如果worker数量不为0,说明还有线程在运行(即使状态是STOP也可能在响应中断)
if (workerCountOf(c) != 0) {
// 中断一个空闲线程(加速退出过程),然后返回,等待该线程退出后再试
interruptIdleWorkers(ONLY_ONE);
return;
}
// ---- 所有条件满足:状态是SHUTDOWN(且队列空)或STOP,并且worker数为0 ----
final ReentrantLock mainLock = this.mainLock;
mainLock.lock(); // 加锁准备修改状态到TIDYING
try {
// 1. CAS尝试将状态设置为TIDYING (workerCount设为0)
if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
try {
// 2. 调用钩子方法 terminated()
terminated();
} finally {
// 3. 将状态设置为TERMINATED
ctl.set(ctlOf(TERMINATED, 0));
// 4. 唤醒所有在termination条件上等待的线程 (调用awaitTermination的线程)
termination.signalAll();
}
return; // 终止完成
}
// CAS失败,说明状态被其他线程修改,回到循环开头重试
} finally {
mainLock.unlock();
}
// else retry on failed CAS
}
}
// termination 是一个Condition对象,用于awaitTermination()的等待和signalAll()的唤醒
private final Condition termination = mainLock.newCondition();
// terminated() 是一个空方法,供子类覆盖,在线程池完全终止前执行清理操作
protected void terminated() { }

关闭流程总结

  • shutdown():设置状态为SHUTDOWN,中断空闲线程,允许运行中和队列中的任务完成。
  • shutdownNow():设置状态为STOP,中断所有线程,清空队列并返回未执行任务。
  • tryTerminate():是状态转换的核心,在满足条件(状态为SHUTDOWN/STOP,worker为0,队列为空(仅SHUTDOWN要求))时,将状态推进到TIDYING -> 执行terminated() -> TERMINATED,并唤醒等待者。

7. 实战指南:如何合理配置线程池

Section titled “7. 实战指南:如何合理配置线程池”

理论和源码都掌握了,现在关键是如何在实际项目中合理地配置ThreadPoolExecutor。没有万能的公式,需要根据具体场景分析和测试。

7.1 线程池大小设置:CPU密集型 vs IO密集型

Section titled “7.1 线程池大小设置:CPU密集型 vs IO密集型”

这是最常见也最核心的配置问题:corePoolSizemaximumPoolSize该设为多少?

前面提到过两个经验公式(N为CPU核心数):

  • CPU密集型任务:线程数 ≈ N + 1
    • 原因:CPU密集型任务长时间占用CPU,过多线程会导致频繁上下文切换,开销大于收益。+1是为了防止偶尔的阻塞(如缺页中断)导致CPU空闲。
    • 例子:视频编码、大量数学运算、正则表达式匹配。
  • IO密集型任务:线程数 ≈ 2N (或更大)
    • 原因:IO密集型任务大部分时间在等待IO(磁盘、网络),CPU处于空闲。需要更多线程来利用CPU的空闲时间片。2N只是一个起点,实际可能需要更大。
    • 更精确估算线程数 = N * (1 + WT/ST),其中WT是线程平均等待时间(IO等待),ST是线程平均计算时间。这个公式更理论化,实际很难精确测量WT和ST。
    • 例子:数据库操作、文件读写、网络API调用、消息队列接收/发送。

实际考量因素 (任务特性、依赖资源、系统负载)

Section titled “实际考量因素 (任务特性、依赖资源、系统负载)”

理论公式只是起点,实际配置需要考虑更多因素:

  1. 任务的混合类型:现实中的任务很少是纯粹的CPU或IO密集型,可能两者兼有。需要分析任务的主要瓶颈在哪里。
  2. 任务的执行时长:短任务和长任务对线程池的需求不同。大量短任务可能需要更多线程来提高吞吐量(类似CachedThreadPool的场景),但要注意线程创建开销和资源消耗。长任务则需要更谨慎地控制线程数。
  3. 任务的依赖性:任务之间是否有依赖?如果任务A的结果是任务B的输入,将它们放在同一个线程池中可能会因线程数不足导致死锁。可能需要拆分到不同线程池或使用其他并发模型(如CompletableFuture)。
  4. 依赖资源的瓶颈:即使是IO密集型任务,线程数也不能无限增加。如果任务依赖的外部资源(如数据库连接池、第三方API并发限制)本身就是瓶颈,再多线程也没用,反而会增加连接竞争或触发限流。此时线程池大小应受限于依赖资源的容量。例如,如果数据库连接池最大只有20个连接,那么执行数据库操作的线程池大小超过20就没有意义了。
  5. 系统资源限制:服务器的内存大小、CPU性能、操作系统对线程数的限制。
  6. 队列的选择和容量:队列容量会影响线程池的行为。大队列可以缓冲更多任务,减少创建非核心线程和触发拒绝策略的几率,但可能增加任务的平均等待时间。小队列则相反。
  7. 可接受的响应时间:业务对任务处理的延迟要求是多少?需要快速响应可能需要更大的corePoolSize或更小的队列。

ThreadPoolExecutor提供了setCorePoolSize()setMaximumPoolSize()方法,允许在运行时动态调整线程池大小。这可以用于根据系统负载变化进行自适应调整,但实现起来比较复杂,需要结合监控数据和合理的调整策略,避免频繁调整导致系统不稳定。通常需要借助监控系统和自动化运维工具来实现。

最终建议

  1. 先分析:判断任务类型(偏CPU还是IO?)、执行时长、依赖关系、外部瓶颈。
  2. 再估算:根据CPU核心数和任务类型,使用经验公式得到一个初始值
  3. 后测试:进行压力测试,模拟真实或预期的负载。
  4. 细监控:在压测和实际运行中,监控关键指标:
    • CPU利用率:是否过高(接近100%)或过低?
    • 内存使用:队列是否积压导致内存增长?
    • 线程池指标getActiveCount()(活跃线程数)、getPoolSize()(当前线程数)、getQueue().size()(队列长度)、getCompletedTaskCount()(完成任务数)、getTaskCount()(总任务数)。
    • 任务平均执行时间任务平均等待时间
    • 拒绝任务数
    • 依赖资源的监控(如DB连接池使用率)。
  5. 终调优:根据监控结果,逐步调整corePoolSize, maximumPoolSize, workQueue容量等参数,找到最佳平衡点。没有银弹,调优是关键。

7.2 队列选择:平衡吞吐量与资源消耗

Section titled “7.2 队列选择:平衡吞吐量与资源消耗”

队列的选择直接影响线程池处理任务的方式:

  • 追求高吞吐、低延迟,但能容忍资源消耗(甚至OOM风险)
    • SynchronousQueue + 较大的maximumPoolSize(类似CachedThreadPool):任务直接传递,快速创建线程响应。适合大量短任务。风险高!
  • 任务量不可预测,希望系统稳定,但有OOM风险
    • 无界LinkedBlockingQueue + 合理的corePoolSize(类似FixedThreadPool):任务无限入队,线程数稳定在corePoolSize风险高!
  • 需要精确控制资源,防止OOM,提供反压
    • 有界ArrayBlockingQueue或有界LinkedBlockingQueue生产环境首选。需要仔细设置容量。
      • 容量设置大:缓冲能力强,减少线程创建和拒绝,但任务等待时间可能变长,内存占用增加。
      • 容量设置小:缓冲能力弱,更快触发创建非核心线程或拒绝策略,任务等待时间短,内存占用少。
  • 任务有优先级
    • PriorityBlockingQueue:但要注意它是无界的,有OOM风险。需要业务上保证任务不会无限增长,或者配合自定义逻辑控制入队。
  • 任务需要延迟执行
    • DelayQueue:用于定时任务场景,同样是无界的。

平衡是关键:通常在有界队列的前提下,通过调整队列容量和**maximumPoolSize**来找到一个平衡点,既能处理峰值流量,又能防止资源耗尽。

7.3 拒绝策略选择:业务容忍度与系统保护

Section titled “7.3 拒绝策略选择:业务容忍度与系统保护”

当线程池真的处理不过来时,如何应对?

  • 任务不能丢,可接受延迟,希望系统自动降速CallerRunsPolicy。提交线程自己执行任务,形成反压。
  • 任务不能丢,需要明确知道失败,由调用方处理AbortPolicy (默认)。抛出异常,调用方捕获处理(重试、记录、降级)。
  • 任务可以丢,优先保证系统稳定DiscardPolicy。静默丢弃。慎用!
  • 优先处理新任务,旧任务可以丢DiscardOldestPolicy。丢弃队列头部任务,尝试接纳新任务。慎用!
  • 有特殊处理逻辑(告警、持久化、放入备用通道):自定义RejectedExecutionHandler

选择依据:业务对任务丢失的容忍度、失败后是否需要补偿、系统在高负载下的保护策略。

7.4 ThreadFactory:让问题排查更轻松

Section titled “7.4 ThreadFactory:让问题排查更轻松”

始终使用自定义ThreadFactory

  • 命名业务名-pool-%d,方便Thread Dump分析。
  • 异常处理:设置UncaughtExceptionHandler,捕获并记录那些未在任务代码中try-catch的异常,避免异常信息丢失。

这是低成本、高回报的最佳实践。

使用ThreadPoolExecutor时,有一些常见的坑需要避免。

8.1 避免使用Executors的预定义线程池

Section titled “8.1 避免使用Executors的预定义线程池”

再次强调:不要使用Executors.newFixedThreadPool(), newSingleThreadExecutor(), newCachedThreadPool()

  • Fixed/Single使用无界队列,可能OOM。
  • Cached线程数可能无限增长,可能OOM或耗尽线程资源。

最佳实践:始终通过ThreadPoolExecutor构造函数创建,明确指定所有核心参数。

  • 使用execute提交的任务,未捕获的异常会由ThreadUncaughtExceptionHandler处理。如果没有设置自定义处理器,默认行为(打印堆栈)可能不足以定位问题,或者异常信息丢失。务必通过ThreadFactory设置统一的处理器。
  • 使用submit提交的任务,异常被FutureTask捕获。必须调用Future.get()才能感知到异常(它会抛出ExecutionException)。如果忘记调用get(),或者Future对象丢失,异常就会被“吞没”。
    • 最佳实践
      • 要么确保对submit返回的Future调用get()并处理ExecutionException
      • 要么在CallableRunnable的任务代码内部进行充分的try-catch
      • 要么继承FutureTask并重写done()方法,在done()中检查异常情况(通过isCancelled()或尝试调用get()捕获异常)。
  • 根源:使用无界队列,或者有界队列设置过大,导致任务生产速度持续大于消费速度。
  • 后果:内存耗尽,OOM。
  • 最佳实践
    • 使用有界队列
    • 合理设置队列容量,进行压测。
    • 监控队列长度,设置告警阈值。
    • 确保消费者(线程池)的处理能力与生产者匹配,或者引入反压机制(如CallerRunsPolicy,或者在提交端进行限流)。
  • 线程复用:线程池的核心优势是复用线程。
  • ThreadLocal陷阱:如果在任务代码中使用了ThreadLocal存储数据,必须在任务执行完毕后(通常在finally块中)调用ThreadLocal.remove()来清理。否则,由于线程被复用,下一个使用该线程的任务可能会读取到上一个任务残留的数据,导致逻辑错误或内存泄漏(如果ThreadLocal存储了大对象,且该线程一直存活)。
ThreadLocal<UserInfo> userInfoThreadLocal = new ThreadLocal<>();
// 在任务中
try {
UserInfo userInfo = ...; // 获取用户信息
userInfoThreadLocal.set(userInfo);
// ... 执行业务逻辑 ...
} finally {
// !!! 极其重要:必须清理ThreadLocal !!!
userInfoThreadLocal.remove();
}
  • 解决方案
    • 手动清理:在finally块中调用remove()
    • 使用框架:一些Web框架(如Spring)会自动处理请求范围内的ThreadLocal清理。
    • 池化ThreadLocal:对于需要频繁创建ThreadLocal值的场景,可以考虑池化技术(较复杂)。
  • 后果:如果线程池(特别是核心线程或使用了非0 keepAliveTime的非核心线程)没有被关闭,它们会一直运行,阻止JVM正常退出。在需要重新部署或停止的应用(如Web应用、定时任务程序)中,这会导致资源无法释放。
  • 最佳实践
    • 确保在应用程序关闭时调用shutdown()shutdownNow()
    • 对于长时间运行的服务,可以在JVM关闭钩子(Runtime.getRuntime().addShutdownHook(...))中执行关闭操作。
    • 使用try-with-resources(如果线程池实现了AutoCloseableThreadPoolExecutor本身没有,但可以通过包装类实现)。
    • 调用shutdown()后,最好配合awaitTermination()等待线程池真正终止,或者在超时后调用shutdownNow()强制关闭。
  • 不要忽略拒绝:即使选择了DiscardPolicyDiscardOldestPolicy,也应该意识到任务正在丢失。最好通过监控或自定义策略记录被拒绝的任务。
  • AbortPolicy的异常处理:如果使用默认策略,确保调用方try-catchRejectedExecutionException,并根据业务决定如何处理(重试、降级、告警等)。
  • 死锁风险:如果线程池中的任务A需要等待任务B的结果,而任务B也在等待进入同一个线程池执行,当线程池已满时,可能会导致死锁(A占着线程等B,B进不来)。
  • 解决方案
    • 拆分线程池:将相互依赖的任务放入不同的线程池。
    • 增大线程池:确保池子足够大,能同时容纳相互依赖的任务(治标不治本)。
    • 使用异步编程模型:如CompletableFuture,可以更好地处理任务之间的依赖和回调,避免阻塞线程。

创建了线程池只是第一步,持续的监控和调优才能确保其高效稳定运行。

9.1 ThreadPoolExecutor提供的监控方法

Section titled “9.1 ThreadPoolExecutor提供的监控方法”

ThreadPoolExecutor提供了一系列方法用于获取其内部状态:

  • getPoolSize(): 当前线程池中的线程数量(包括正在执行和空闲的)。
  • getActiveCount(): 当前正在执行任务的线程的大约数量。
  • getCorePoolSize(): 返回核心线程数。
  • getMaximumPoolSize(): 返回最大线程数。
  • getLargestPoolSize(): 线程池生命周期内曾经达到的最大线程数(可用于判断峰值)。
  • getQueue().size(): 当前队列中等待执行的任务数量。
  • getQueue().remainingCapacity(): 队列剩余容量(仅对有界队列有意义)。
  • getTaskCount(): 线程池曾经接收到的任务总数(近似值)。
  • getCompletedTaskCount(): 线程池已完成执行的任务总数(近似值)。
  • isShutdown(): 线程池是否已调用shutdown()
  • isTerminating(): 线程池是否正在终止(状态为SHUTDOWNSTOP,但未到TERMINATED)。
  • isTerminated(): 线程池是否已完全终止。

如何使用:可以将这些指标定期采集,并集成到监控系统中(如Prometheus + Grafana, Zabbix, Metrics库等),绘制趋势图,设置告警规则。

Java Management Extensions (JMX) 是一种标准的Java应用监控和管理框架。可以通过JMX MBean将线程池的监控指标和管理操作(如动态调整参数、关闭)暴露出来,供JMX客户端(如JConsole, VisualVM, Jolokia等)访问。

优点:标准化、远程访问、可集成到现有JMX监控体系。
实现

  1. 创建一个MBean接口,定义需要暴露的属性(指标)和操作。
  2. 创建一个实现该接口的MBean类,内部持有ThreadPoolExecutor实例,并实现获取指标和执行操作的逻辑。
  3. 将MBean实例注册到MBean Server中。

许多框架(如Spring Boot Actuator)已经内置了对ThreadPoolExecutor的JMX监控支持,可以简化配置。

9.3 动态调整参数 (setCorePoolSize, setMaximumPoolSize)

Section titled “9.3 动态调整参数 (setCorePoolSize, setMaximumPoolSize)”

ThreadPoolExecutor允许在运行时调整核心和最大线程数:

  • setCorePoolSize(int corePoolSize)
  • setMaximumPoolSize(int maximumPoolSize)

注意事项

  • 调整corePoolSize
    • 调大:如果当前线程数小于新的corePoolSize,且队列中有任务,可能会立即创建新的核心线程。
    • 调小:多余的核心线程不会立即销毁,它们会等到空闲超时(如果allowCoreThreadTimeOut为true)或者自然消亡(如果keepAliveTime对核心线程无效)。
  • 调整maximumPoolSize
    • 调大:允许线程池在队列满时创建更多非核心线程。
    • 调小:如果当前线程数超过了新的maximumPoolSize,多余的线程(无论核心还是非核心)会在空闲时(根据keepAliveTime)被回收,直到满足新的上限。getTask()中的逻辑会处理这种情况。
  • 平滑性:调整不是瞬间完成的,需要时间让线程池达到新的状态。频繁调整可能导致线程池状态波动。
  • 队列交互:调整大小需要配合队列容量和类型考虑。例如,对于无界队列,maximumPoolSize几乎无效。
  • 复杂度:实现动态调整需要可靠的监控数据和智能的决策逻辑,否则可能适得其反。

应用场景:适用于负载波动非常大且有明显规律(如白天高峰,夜间低谷),或者能根据外部信号(如队列长度、响应时间)进行反馈调节的系统。

关注以下关键指标组合:

  • 队列长度 (queue.size):持续增长表示处理能力不足;过高可能OOM;长时间为0可能资源利用率低。
  • 活跃线程数 (activeCount) vs 当前线程数 (poolSize)activeCount接近poolSize表示线程利用率高;如果activeCount远小于poolSizepoolSize接近maximumPoolSize,可能keepAliveTime设置过长或峰值已过。
  • 完成任务数 (completedTaskCount) 的增长速率:反映了线程池的吞吐量。
  • 任务平均等待时间 + 平均执行时间:反映了任务处理的延迟。可以通过 AOP 或在任务代码中埋点计算。
  • 拒绝次数:非0表示线程池曾经饱和,需要分析原因(是正常峰值还是处理能力不足?)。
  • CPU / 内存利用率:结合系统资源监控,判断瓶颈。

调优目标:在满足业务响应时间要求的前提下,尽量提高资源利用率(CPU),同时避免队列过度积压和任务拒绝。这是一个需要不断迭代和权衡的过程。

ThreadPoolExecutor还有一些不常用但有用的特性。

10.1 allowCoreThreadTimeOut(boolean value)

Section titled “10.1 allowCoreThreadTimeOut(boolean value)”
  • 设置是否允许核心线程超时回收。
  • 默认为false,核心线程永不回收。
  • 设置为true后,核心线程也会在空闲时间达到keepAliveTime后被回收。
  • 应用场景:任务负载波动极大,希望在系统完全空闲时释放所有线程资源,对资源极其敏感的场景。

10.2 prestartCoreThread() & prestartAllCoreThreads()

Section titled “10.2 prestartCoreThread() & prestartAllCoreThreads()”
  • prestartCoreThread(): 启动一个核心线程,即使没有任务提交。如果核心线程已满则返回false
  • prestartAllCoreThreads(): 启动所有核心线程。返回成功启动的线程数。
  • 作用:预热线程池。默认情况下,核心线程是按需创建的。调用这些方法可以在线程池创建后立即启动核心线程,让它们处于等待任务的状态,减少第一个任务到来时的冷启动延迟。
  • 应用场景:对任务响应时间要求非常高的场景,希望避免首次任务提交时的线程创建开销。
  • protected void terminated() {}
  • 这是一个空方法,设计用于子类覆盖
  • 调用时机:在线程池状态进入TERMINATED之前(即从TIDYING转换时),在tryTerminate()方法内部,持有全局锁mainLock的情况下被调用。
  • 作用:允许在线程池完全终止、所有任务和线程都结束后,执行一些自定义的清理或资源释放操作。例如,关闭与线程池关联的其他资源。
  • 注意:此方法在锁内执行,不应执行耗时过长或可能阻塞的操作。

ThreadPoolExecutor是Java并发编程的快刀了属于是,它强大、灵活,但也相对复杂。掌握它需要理解其核心参数、工作原理、状态转换和源码细节。
这块部分多重要应该不需要多说了,学就完事了奥利给!
核心要点回顾

  • 目的:复用线程,管理资源,提高性能,简化并发编程。
  • 七大参数corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, rejectedExecutionHandler 是定制线程池行为的关键。
  • 工作流程:核心线程 -> 队列 -> 非核心线程 -> 拒绝策略。
  • 队列选择:对线程池行为影响巨大,优先使用有界队列
  • 拒绝策略:根据业务容忍度和系统保护需求选择。
  • ThreadFactory:务必自定义,方便命名和异常处理。
  • Executors风险:避免使用Executors创建线程池,防止OOM。
  • 异常处理:注意executesubmit的异常处理差异。
  • ThreadLocal清理:线程复用场景下的常见陷阱。
  • 关闭:确保应用程序退出时关闭线程池。
  • 监控与调优:持续监控是保证线程池健康运行的关键,没有银弹,需要测试和调整。

Happy coding!