摘要:本篇文章围绕 Java 并发编程中的线程池核心内容,梳理了线程池的核心特性,还介绍了 Executor 工具包下的线程池,最后介绍了ScheduledThreadPoolExecutor线程池。

第9章 Java 中的线程池

核心定位

Java 线程池是应用最广泛的并发框架,是处理异步和并发任务的基础工具。它的核心价值是通过池化复用线程,解决了线程频繁创建销毁的性能问题,同时提升了系统的可管理性。

三大核心优势

  1. 降低资源消耗:线程池会复用已创建的线程,避免了线程创建和销毁带来的系统开销,减少了内存与 CPU 资源的浪费。

  2. 提高响应速度:任务到达时可直接使用池中的"热备"线程执行,无需等待线程创建,从而缩短任务响应延迟。

  3. 便于管理:线程是稀缺资源,无限制创建会导致资源耗尽和系统不稳定。线程池可通过参数控制线程数量,实现统一分配、调优和监控,保障系统稳定。

9.2.1 线程池的创建

线程池的参数有哪些?

线程池的构造函数有7个参数:

  • corePoolSize:线程池核心线程数量。默认情况下,线程池中线程的数量如果 <= corePoolSize,那么即使这些线程处于空闲状态,那也不会被销毁。

  • maximumPoolSize限制了线程池能创建的最大线程总数(包括核心线程和非核心线程),当 corePoolSize 已满 并且 尝试将新任务加入阻塞队列失败(即队列已满)并且 当前线程数 < maximumPoolSize,就会创建新线程执行此任务。

  • keepAliveTime临时线程(超过 corePoolSize 的线程)在空闲状态下的最大存活时间。当线程池中线程的数量大于corePoolSize,并且某个线程的空闲时间超过了keepAliveTime,那么这个线程就会被销毁。

  • unit:就是keepAliveTime时间的单位。

  • workQueue:工作队列。当没有空闲的线程执行新任务时,该任务就会被放入工作队列中,等待执行。

  • threadFactory:线程工厂。可以用来给线程取名字等。

  • handler:拒绝策略。当一个新任务交给线程池,如果此时线程池中有空闲的线程,就会直接执行,如果没有空闲的线程,就会将该任务加入到阻塞队列中,如果阻塞队列满了,就会创建一个新线程,从阻塞队列头部取出一个任务来执行,并将新任务加入到阻塞队列末尾。如果当前线程池中线程的数量等于maximumPoolSize,就不会创建新线程,就会去执行拒绝策略。

这张图是书中对 ThreadPoolExecutor 核心执行逻辑的梳理,完整展示了任务提交后的处理路径。

流程核心逻辑

  1. 提交任务:外部线程向线程池提交一个新的 Runnable 任务,触发 execute() 方法。

  2. 判断核心线程池是否已满

    1. :直接创建新的核心线程执行该任务。

    2. :进入下一步判断。

  3. 判断任务队列是否已满

    1. :将任务存入阻塞队列等待空闲线程执行。

    2. :进入下一步判断。

  4. 判断线程池是否已达最大线程数

    1. :创建新的非核心线程执行该任务。

    2. :触发拒绝策略。

  5. 执行拒绝策略:当线程池和队列都满时,会按照预设的拒绝策略处理无法执行的任务,常见策略包括抛出异常、直接丢弃、丢弃队列最早任务、让调用者自行执行等。


1)如果当前运行的线程少于corePoolSize,则创建新线程来执行任务 (执行此步骤需获取全局锁)。

2)如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。

3)如果无法将任务加入BlockingQueue(队列已满),则创建新线程来处理任务(执行此步骤需获取全局锁)。

4)如果创建新线程将使当前运行的线程超出maximumPoolSize,会按照预设的拒绝策略处理无法执行的任务。


为什么第一步需要全局锁?

当线程池还没达到corePoolSize时,每创建一个新的核心线程,都需要保证线程数统计的准确性。

  • 比如线程池的corePoolSize=5,此时已有 3 个核心线程,同时有 10 个任务并发提交。

  • 如果不加全局锁,可能会出现 "多个任务同时判断线程数 < 5,然后同时创建线程" 的情况,最终创建出远超过 5 个核心线程的结果,违背corePoolSize的设计初衷。

  • 因此,创建新核心线程(第一步)必须加全局锁(比如ReentrantLock),确保同一时间只有一个任务能创建核心线程,保证线程数的精确控制。


采取上述步骤的总体设计思路,是为了在执行 execute()方法时,尽可能地避免获取全局锁(那将会是一个严重的可伸缩瓶颈)。ThreadPoolExecutor完成预热之后(当前运行的线程数大于等于corePoolSize),几乎所有的execute()方法调用都是执行步骤2, 而步骤2不需要获取全局锁。

把第二步(入队列)设计在第三步(创建非核心线程)之前,核心目的就是为了最大化规避全局锁,同时兼顾资源效率,这是 ThreadPoolExecutor 设计中最关键的优化思路之一。

1.ArrayBlockingQueue

它是基于数组实现的阻塞队列,必须在初始化时指定固定容量,属于严格的有界队列,遵循先进先出(FIFO)的排序规则。核心特点是读写操作共用同一把锁,因此并发性能稳定但吞吐量处于中等水平,不会出现突发的性能波动。适合对任务数量有明确上限、需要严格控制队列长度,避免任务无限堆积的场景。

2.LinkedBlockingQueue

基于链表实现,默认容量为Integer.MAX_VALUE(日常使用中可视为无界队列),也可手动指定容量变为有界,同样遵循先进先出(FIFO)规则。它的核心优势是读写操作使用分离锁(读锁和写锁分开),因此吞吐量通常高于 ArrayBlockingQueue。是Executors.newFixedThreadPool()的默认队列,适合任务量较大、希望缓冲更多任务,且对队列容量没有严格限制的场景。

3.SynchronousQueue

这是一种特殊的队列没有实际的存储结构、容量为 0,不存在 "缓冲任务" 的概念,也没有固定的排序规则。它的核心特性是 "直接移交":插入任务的操作必须等待有线程来取走任务,否则会阻塞;任务会直接从生产者线程移交到消费者线程,无任何缓冲开销。是Executors.newCachedThreadPool()的默认队列,适合任务处理速度快、任务量波动大(高峰期任务多、低峰期几乎无任务)的场景。

4.PriorityBlockingQueue

基于结构实现,属于无界队列,不遵循先进先出规则,而是按照自定义优先级排序执行任务(需要任务实现Comparable接口来定义优先级)。核心特点是能优先处理高优先级任务,哪怕高优先级任务提交较晚,也会插队执行。适合任务有明确优先级高低区分、需要优先处理核心 / 重要任务的场景(比如订单处理中,VIP 用户订单优先于普通用户订单)。

线程池工作队列满了有哪些拒接策略?

当线程池的任务队列满了之后,线程池会执行指定的拒绝策略来应对,常用的四种拒绝策略包括:CallerRunsPolicy、AbortPolicy、DiscardPolicy、DiscardOldestPolicy,此外,还可以通过实现RejectedExecutionHandler接口来自定义拒绝策略。

四种预置的拒绝策略:

  • AbortPolicy,直接抛出一个任务被线程池拒绝的异常。(默认拒绝策略)

  • DiscardPolicy,不做任何处理,静默拒绝提交的任务。

  • DiscardOldestPolicy,抛弃最老的任务,然后执行该任务。

  • CallerRunsPolicy,使用线程池的调用者所在的线程去执行被拒绝的任务,除非线程池被停止或者线程池的任务队列已有空缺。

  • 自定义拒绝策略,通过实现接口可以自定义任务拒绝策略。

9.2.2 向线程池提交任务


一、execute() 方法

execute() 是线程池最基础的任务提交方法,专门用于提交不需要返回值的任务。

  • 任务类型:仅支持 Runnable 类型的任务,这类任务只执行逻辑,不返回结果。

  • 核心特点:提交后无法直接判断任务是否执行成功,也无法获取任务的执行结果。


二、submit() 方法

submit() 是增强型的任务提交方法,用于提交需要返回值的任务。

  • 任务类型:支持 RunnableCallable 两种类型的任务。Callable 任务可以通过 call() 方法返回结果。

  • 核心特点

    • 提交后会返回一个 Future 对象,通过它可以判断任务是否执行成功。

    • 调用 future.get() 会阻塞当前线程,直到任务完成并返回结果;带超时参数的 get(long timeout, TimeUnit unit) 会在超时后立即返回,此时任务可能未执行完毕。

    • 可以捕获任务执行中的异常(如中断异常、任务执行异常),便于错误处理。

Future<Object> future = executor.submit(new Callable<Object>() {
    @Override
    public Object call() throws Exception {
    // 执行有返回值的任务逻辑
    return "任务执行结果";
    }});
    
    try {Object result = future.get(); 
    // 阻塞等待结果
    } catch (InterruptedException e) {
    // 处理线程中断异常
    } catch (ExecutionException e) {
    // 处理任务执行异常
    } finally {
         // 关闭线程池
        executor.shutdown(); 
    }

9.2.3 关闭线程池

shutdown() 方法(温和关闭)

  1. 状态变更:将线程池的状态设置为 SHUTDOWN

  2. 中断逻辑:仅中断所有没有正在执行任务的空闲线程。

  3. 任务处理:已提交的任务(包括正在执行和队列中等待的)会继续执行完毕。

  4. 返回值:无返回值。

  5. 适用场景:希望等待所有已提交任务正常完成后再关闭线程池的场景,比如系统优雅停机、批处理任务结束后清理资源。


shutdownNow() 方法(强制关闭)

  1. 状态变更:将线程池的状态设置为 STOP

  2. 中断逻辑:尝试停止所有正在执行或暂停任务的线程(包括正在工作的线程)。

  3. 任务处理:不再处理队列中等待的任务,并将这些任务组成列表返回。

  4. 返回值:返回等待执行的任务列表,便于后续处理。

9.2.4 合理地配置线程池

按任务性质的核心配置策略

1.CPU 密集型任务

  • 特点:任务持续占用 CPU 进行计算,几乎不涉及 IO 等待。

  • 配置建议:线程数应尽可能小,推荐公式为 CPU核心数 + 1

  • 原理:过多线程会导致频繁的上下文切换,反而降低 CPU 利用率。

2.IO 密集型任务

  • 特点:任务大部分时间在等待 IO 操作(如数据库查询、网络请求),CPU 处于空闲状态。

  • 配置建议:线程数应尽可能多,推荐公式为 2 * CPU核心数

  • 原理:更多线程可以在 IO 等待时切换执行,充分利用 CPU 资源。

3.混合型任务

  • 特点:任务同时包含 CPU 计算和 IO 等待。

  • 配置建议:如果可以拆分,将其拆分为独立的 CPU 密集型任务和 IO 密集型任务。

  • 原理:拆分后两个任务可并行执行,能显著提升整体吞吐量。

第10章 Executor 工具包

ThreadPoolExecutor详解

FixedThreadPool


SingleThreadExecutor


CachedThreadPool

ScheduledThreadPoolExecutor


使用ScheduledThreadPoolExecutor实现延时任务

import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

@Slf4j(topic = "c.DelayTaskDemo")
public class DelayTaskDemo {

    public static void main(String[] args) {
        // 1. 创建定时任务线程池(核心线程数=1)
        ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(1);
        log.info("程序启动,开始提交延迟任务...");

        // 2. 提交延迟任务(核心方法:schedule)
        // 参数说明:
        // - 第一个参数:要执行的延迟任务逻辑(Lambda表达式)
        // - 第二个参数:延迟时间(这里设置为5秒)
        // - 第三个参数:时间单位(这里是秒)
        scheduledPool.schedule(() -> {
            // 延迟任务的核心业务逻辑
            try {
                log.info("延迟任务开始执行:模拟订单超时关闭");
                // 这里写实际业务逻辑,比如:更新订单状态为“已取消”、释放库存等
                Thread.sleep(1000); // 模拟业务执行耗时
                log.info("延迟任务执行完成:订单已关闭");
            } catch (Exception e) {
                log.error("延迟任务执行失败", e);
            }
        }, 5, TimeUnit.SECONDS);
    }
}

恭喜你学习完本节内容!✿

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐