Java 线程池介绍

本文将介绍Java 线程池,线程池在处理多线程的时候是非常由用的工具。学好线程池,可以让我们对线程的掌控能力上一个台阶。

文章目录

1 线程池的概述

线程池是Java并发编程中的一种重要工具,其主要功能是有效地管理和复用线程资源,以避免频繁创建和销毁线程所带来的性能开销。

1.1 线程池的重要性

线程池的掌握对于Java工程师来说至关重要。许多问题的产生都源于我们对线程池的使用不当。在求职过程中,线程池也是面试中常见的考察点。面试官可以通过对线程池的提问,逐层深入地考察求职者对线程池的理解和思考,因此,线程池是面试中的高频考点。

1.2 “池”的概念

在软件中,“池”可以理解为一种计划经济的概念。这个“池”的概念不仅出现在线程池中,也出现在如连接池中。以计划经济为例,由于资源有限,需要通过粮票、布票和油票等进行分配,以防止资源被富人独占,这就是计划经济的设定。

类似地,线程池的设定也是基于相同的理念。线程资源是有限的,例如,如果只有10个线程,那么就可以创建一个包含10个线程的线程池。尽管可能有大量的任务需要执行,例如1000个或2000个任务,但是我们可以将这些任务分配到这10个线程中去执行。这样,虽然执行所有任务需要一定的时间,但是这10个线程的总量是被控制住的。

此外,线程池还有一个优点,即不需要创建过多的线程。每个线程的创建都会带来很大的开销,而线程池可以控制线程的总量,并复用每个线程。这就像经过一段时间训练的工人,他们的敏捷度和熟练度都会提高。因此,这10个线程在执行任务时效率很高,因为不需要重新创建,也不需要销毁,只需要不断地执行任务即可。

总结来说,线程池有两个主要优点:

  1. 可以控制资源的总量
  2. 可以复用每个线程

1.3 如果不使用线程池,每个任务都新开一个线程处理

假设我们没有使用线程池,而是对每个到来的任务都新开一个线程进行处理。如果只有一个任务,我们只需创建一个线程;如果有多个任务,我们需要使用for循环创建多个线程。以下是两种创建线程的方式的代码示例。

1.3.1 单个线程

public class EveryTaskOneThread {
    public static void main(String[] args) {
        Task task = new Task();
        Thread thread = new Thread(task);
        thread.start();
    }

    static class Task implements Runnable{
        @Override
        public void run() {
            System.out.println("执行了任务");
        }
    }
}

输出结果如下:
在这里插入图片描述

1.3.2 使用for循环创建线程

public class ForLoop {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Task task = new Task();
            Thread thread = new Thread(task);
            thread.start();
        }
    }

    static class Task implements Runnable{
        @Override
        public void run() {
            System.out.println("执行了任务");
        }
    }
}

执行结果如下:
在这里插入图片描述

1.3.3 当任务量上升到1000

上述两种方式在任务量较小的情况下是可行的。但是,如果任务量上升到1000个,我们就需要深入思考了。虽然我们可以将for循环的结束条件设置为1000,但这是一种笨拙的方法。因为在Java中,每一个线程都会直接对应到操作系统中的一个线程,这是一种1:1的线程模型。这样一来,我们就会在操作系统中创建1000个线程,这将带来很大的开销。线程的生命周期开销是非常高的,创建和销毁线程都需要JVM和操作系统进行一些辅助操作。在这种大量创建线程的情况下,会消耗大量的资源,尤其是内存。这些线程在创建后还需要被回收,这将给垃圾回收器带来压力。我们的系统内存是有上限的,线程也会占用一定的内存,所以创建的线程数量是有上限的。如果任务数量继续增加,我们就无法创建相应的线程来处理这些任务。例如,如果任务数量从1000个增加到2万个,或者30万个,我们的线程数量就可能超过上限,导致出错,甚至抛出内存不足的异常。

为了避免这种情况,我们希望使用固定数量的线程来执行这1000个任务,这样就可以避免反复创建和销毁线程所带来的开销。因此,如果有大量的任务需要处理,我们并不需要为每个任务都创建一个新的线程,而应该使用线程池。

1.4 为什么要使用线程池?

通过前面的讨论,我们可以明确地回答这个问题。

1.4.1 一个任务对应一个线程的问题

首先,让每个任务都创建一个线程会带来两个问题:

  1. 过多的线程会占用过多的内存。
  2. 反复创建和销毁线程的开销大。

1.4.2 解决问题的思路

针对这两个问题,我们有以下解决思路:

  • 针对问题1:只创建少量的线程,避免内存占用过多。
  • 针对问题2:让这些线程始终保持工作状态,并反复执行任务,避免生命周期的损耗。

1.4.3 线程池的好处

总结一下,使用线程池的好处包括:

  • 加快响应速度:由于不再需要反复创建和销毁线程,可以消除这部分延迟,从而提高用户体验。
  • 合理利用CPU和内存:CPU和内存资源是有限的,通过线程池可以灵活调整线程的数量,既不会因为线程过多而导致内存溢出,也不会因为线程过少而浪费CPU和内存资源。我们可以灵活地掌握线程的数量,以达到一个平衡,使效率达到最高点。
  • 统一管理:假设有3000个线程在执行任务,如果我们想在任务执行到一半时停止线程的运行,线程池可以方便地进行统一管理,而不需要我们一个一个去停止线程的运行。这样不仅方便管理,还便于资源统计。

1.5 线程池适用的场景

接下来,我们来看一下哪些场景适合使用线程池。

1.5.1 服务器接收到大量请求时

这是线程池最常用的场景。服务器会接收到大量的客户端请求,如果使用线程池,可以大大减少线程的创建和销毁次数,从而提高服务器的工作效率。实际上,基于Java语言的Web服务器,如Tomcat,都是使用线程池实现的。

1.5.2 需要创建多个线程的情况

在实际开发中,如果需要创建5个以上的线程,就可以使用线程池进行管理。例如,如果我们需要同时请求10个接口,并对返回的结果进行进一步处理,那么这种情况就非常适合使用线程池。

2 创建和停止线程池

在本节中,我们将学习如何创建和停止线程池。

2.1 创建线程池

2.1.1 线程池的构造函数

线程池的构造函数参数相对复杂,尤其是与JDK中其他一些类的构造函数相比。例如,创建一个 HashMap 时,构造函数可以不传递任何参数,而线程池的构造函数则有多个参数,并且每个参数都需要理解透彻,才能正确地创建和使用线程池。

下表列出了线程池构造函数的主要参数:

参数名类型含义
corePoolSizeint核心线程数量
maximumPoolSizeint最大线程数量
keepAliveTimelong非核心线程的存活时间
workQueueBlockingQueue任务存储队列
threadFactoryThreadFactory用于创建新线程的工厂
handlerRejectedExecutionHandler当线程池无法接收新任务时的拒绝策略

这些参数是创建线程池时需要掌握的,接下来我们将逐一介绍这些参数。

一、corePoolSize 和 maximumPoolSize

如果你之前没有接触过线程池,可能会觉得它的设计非常巧妙。首先,我们来看这两个参数的区别及其含义:

  • corePoolSize:核心线程数。线程池在初始化后,默认情况下没有任何线程,只有当任务到来时,线程池才会创建新线程来执行任务。假设 corePoolSize=5,当有5个任务到来时,线程池会创建5个线程来处理这些任务。这是核心线程的基本工作方式。

  • maximumPoolSize:最大线程数。线程池在核心线程数的基础上,可以额外创建一些非核心线程来处理任务。这些非核心线程的数量上限由 maximumPoolSize 决定。任务的数量往往是不均匀的,比如在某些高峰期(如双十一、秒杀场景),任务量可能会激增,而在平时则较少。为了应对这种不均匀的任务负载,线程池引入了 maximumPoolSize 来灵活扩展线程数量。假设 corePoolSize=5,线程池已经创建了5个核心线程,这些线程会一直存活。如果有更多任务到来,线程池可以创建非核心线程来处理这些任务,但非核心线程的数量不会超过 maximumPoolSize

如下图所示,左侧是核心线程数量。当有任务到来时,线程池会创建新的线程,直到核心线程数达到 corePoolSize。此时,如果有更多任务到来,线程池不会再创建新的核心线程,而是将任务放入阻塞队列中,等待空闲的核心线程来处理这些任务。

线程池工作原理图

例如,假设 corePoolSize=5,当5个核心线程都在工作时,如果再有5个任务到来,线程池会将这些任务放入阻塞队列中,等待核心线程处理完当前任务后再从队列中取出任务继续执行。在这种情况下,线程池不会轻易创建新的线程,线程数量不会轻易突破 corePoolSize

然而,当任务量继续增加,阻塞队列也满了(假设队列容量为10),此时线程池会创建非核心线程来处理这些任务。非核心线程的数量最多可以扩展到 maximumPoolSize。因此,线程池的线程数量可以根据任务量动态扩展。

公式如下:

非核心线程数量 = m a x i m u m P o o l S i z e − c o r e P o o l S i z e 非核心线程数量 = maximumPoolSize - corePoolSize 非核心线程数量=maximumPoolSizecorePoolSize

如果 maximumPoolSize == corePoolSize,则非核心线程数量为0,即线程池不会创建非核心线程。

此外,线程池的总线程数量可以表示为:

线程池当前线程数量 = 核心线程数量 + 非核心线程数量 线程池当前线程数量 = 核心线程数量 + 非核心线程数量 线程池当前线程数量=核心线程数量+非核心线程数量

二、添加线程的规则

总结一下线程池添加线程的规则:

  1. 如果当前线程数量小于 corePoolSize,即使有空闲线程,线程池也会创建新线程来执行新任务。
  2. 如果当前线程数量大于等于 corePoolSize 但小于 maximumPoolSize,新任务到来时,如果没有空闲线程,线程池会将任务放入工作队列。
  3. 如果工作队列已满,且当前线程数量小于 maximumPoolSize,线程池会创建新的线程来执行新任务。
  4. 如果工作队列已满,且当前线程数量等于 maximumPoolSize,线程池会根据拒绝策略拒绝新任务。

通过这些规则,线程池能够灵活地管理线程数量,以应对不同的任务负载。

workQueue 是线程池中的任务存储队列,用于存放等待执行的任务。当线程池中的所有核心线程都在忙碌时,新的任务会被放入这个队列中,等待空闲线程来处理。如果队列满了,并且线程池中的线程数量已经达到了 maximumPoolSize,此时线程池将使用拒绝策略(handler)来处理新任务。

为了更清晰地理解线程池添加线程的规则,下面是一张流程图,展示了线程池在处理任务时的逻辑:

线程池任务处理流程图

可以通过以下简单的判断顺序来记忆线程池的任务处理逻辑:

线程池任务处理判断顺序

为了更好地理解线程池的工作机制,我们可以用一个日常生活中的例子来类比:

假设你去吃烧烤,秋天的天气不冷不热,大家通常会选择在店里吃。店里有5张桌子,这5张桌子始终存在,对应于线程池中的 corePoolSize(核心线程数)。如果店里的桌子坐满了,新的客人就需要排队等待,这相当于任务被放入 workQueue(工作队列)。如果排队的人越来越多,店家可能会在店外临时摆放一些桌子,这些临时桌子对应于 maximumPoolSize(最大线程数)。但外面的桌子数量也是有限的,不能无限增加。等到收摊时,外面的桌子会被收回,而店里的桌子始终存在,这对应于非核心线程在空闲时会被销毁,而核心线程则会一直保留。

假设线程池的配置如下:

  • corePoolSize = 5
  • maximumPoolSize = 10
  • 队列容量为 100

当任务开始提交时,线程池最多会创建5个核心线程来处理任务,超过的任务会被放入队列中,直到队列达到100个任务。当队列满了,新的任务继续进来时,线程池会创建非核心线程,直到线程池中的线程数量(核心线程 + 非核心线程)达到10。如果还有任务进来,线程池将拒绝这些任务。

三、增减线程的特点

虽然我们已经了解了线程池的规则和流程,但线程池的设计也有一些特点和潜在的缺点。下面总结线程池在增减线程时的几个特点:

  1. 固定大小的线程池
    通过将 corePoolSizemaximumPoolSize 设置为相同的值,可以创建一个固定大小的线程池。根据公式:

    非核心线程数量 = m a x i m u m P o o l S i z e − c o r e P o o l S i z e 非核心线程数量 = maximumPoolSize - corePoolSize 非核心线程数量=maximumPoolSizecorePoolSize

    如果 corePoolSizemaximumPoolSize 相等,则非核心线程数量为 0。这意味着线程池在创建到核心线程数量后,不会再增加新的线程,也不会销毁现有的线程,线程池的线程数量始终保持为 corePoolSize。因此,这种配置下的线程池是固定大小的。

  2. 线程池倾向于保持较少的线程数量
    线程池的设计目的是尽量保持较少的线程数量,只有在负载非常大的情况下才会增加新的线程。具体来说,只有当工作队列满了时,线程池才会考虑扩展非核心线程。因此,线程池更倾向于将任务放入队列,而不是立即创建新的线程来处理任务。

  3. 通过设置 maximumPoolSizeInteger.MAX_VALUE,线程池可以容纳任意数量的并发任务
    如果将 maximumPoolSize 设置为 Integer.MAX_VALUE,线程池可以容纳几乎无限数量的并发任务。即使工作队列满了,线程池仍然可以通过创建新的线程来处理任务,而不会拒绝任务。由于 Integer.MAX_VALUE 是一个非常大的数值,通常任务数量不会达到这个上限,因此线程池的容量几乎不受限制。

  4. 使用无界队列时,线程数量永远不会超过 corePoolSize
    如果使用无界队列(例如 LinkedBlockingQueue),线程池的线程数量永远不会超过 corePoolSize。这是因为创建非核心线程的条件是工作队列满了,但无界队列永远不会满,因此任务会一直被放入队列中,而不会触发创建非核心线程的条件。在这种情况下,maximumPoolSize 参数将失去作用,线程池的线程数量始终保持为 corePoolSize

通过对 corePoolSizemaximumPoolSizekeepAliveTimeworkQueue 的详细分析,我们可以看到线程池的设计是为了在处理并发任务时,能够灵活地管理线程数量。线程池通过工作队列和非核心线程的动态扩展,能够应对不同的任务负载。同时,线程池的配置也可以根据具体需求进行调整,例如创建固定大小的线程池、使用无界队列等。

四、keepAliveTime

接下来我们来看一下 keepAliveTime 参数。它的含义是线程的保持存活时间。具体来说,当线程池的线程数量超过 corePoolSize 时,多余的线程在空闲时间超过 keepAliveTime 后会被终止并回收。这种机制能够在任务负载减少时减少资源浪费。

keepAliveTime 的作用:

  • 当线程池中的线程数量大于 corePoolSize 时,空闲的非核心线程如果超过 keepAliveTime 没有被使用,就会被终止。
  • 这种机制确保了在任务量减少时,线程池能够回收多余的线程,避免资源浪费。
  • 需要注意的是,默认情况下,只有非核心线程会被回收,核心线程即使空闲时间超过 keepAliveTime 也不会被回收。不过,可以通过 allowCoreThreadTimeOut 方法设置允许核心线程超时回收。

allowCoreThreadTimeOut 方法

allowCoreThreadTimeOut 方法允许我们设置核心线程是否可以在空闲时间超过 keepAliveTime 后被回收。其源码如下:

/**
 * 设置策略,决定核心线程是否可以在没有任务到达的情况下,在保持活动时间内超时并终止,
 * 如果需要,当有新任务到达时可以替换它们。当值为 false 时,核心线程不会因为缺少任务而被终止。
 * 当值为 true 时,应用于非核心线程的相同保持活动策略也适用于核心线程。为了避免线程不断被替换,
 * 在设置为 {@code true} 时,保持活动时间必须大于零。通常应在线程池被实际使用之前调用此方法。
 *
 * @param value {@code true} 表示核心线程应该超时,否则为 {@code false}
 * @throws IllegalArgumentException 如果 value 为 {@code true} 且当前保持活动时间不大于零
 *
 * @since 1.6
 */
public void allowCoreThreadTimeOut(boolean value) {
    // 如果传入的值为 true,并且 keepAliveTime 小于等于 0,则抛出异常
    // 这是因为核心线程必须有一个大于 0 的存活时间(keepAliveTime)
    if (value && keepAliveTime <= 0)
        throw new IllegalArgumentException("Core threads must have nonzero keep alive times");

    // 如果传入的值与当前 allowCoreThreadTimeOut 的值不同,才进行修改
    if (value != allowCoreThreadTimeOut) {
        // 更新 allowCoreThreadTimeOut 的值
        allowCoreThreadTimeOut = value;

        // 如果传入的值为 true,表示允许核心线程超时
        // 那么需要中断那些空闲的工作线程
        if (value)
            interruptIdleWorkers();
    }
}

关键点:

  • allowCoreThreadTimeOut 设置为 true 时,核心线程也会在空闲时间超过 keepAliveTime 后被回收。
  • 如果 keepAliveTime 小于等于 0 且 allowCoreThreadTimeOut 被设置为 true,则会抛出异常,因为核心线程必须有一个大于 0 的存活时间。
  • 该方法通常在线程池实际使用之前调用,以确保线程池的行为符合预期。

通过合理配置 keepAliveTimeallowCoreThreadTimeOut,我们可以更灵活地控制线程池的资源使用,确保在任务负载变化时能够动态调整线程数量,避免资源浪费。

五、ThreadFactory 用来创建线程

ThreadFactory 是用于创建线程的工厂类。在创建线程池时,可以通过传入一个 ThreadFactory 来定制线程的创建方式。默认情况下,线程池使用 Executors.defaultThreadFactory() 来创建线程。通过该默认工厂创建的线程具有以下特点:

  • 所有线程都属于同一个线程组。
  • 线程的优先级为 Thread.NORM_PRIORITY(即普通优先级)。
  • 线程不是守护线程。

如果需要自定义线程的名称、线程组、优先级,或者将线程设置为守护线程,可以通过实现 ThreadFactory 接口并传入自定义的工厂类。

ThreadFactory 是一个接口,定义如下:

public interface ThreadFactory {

    /**
     * 创建一个新的 {@code Thread}。实现类可以同时初始化线程的优先级、名称、
     * 守护线程状态、{@code ThreadGroup} 等等。
     *
     * @param r 一个由新线程实例执行的 Runnable 对象
     * @return 构造的线程,或者如果创建线程的请求被拒绝则返回 {@code null}
     */
    Thread newThread(Runnable r);
}

实现 ThreadFactory 接口的类必须重写 newThread 方法,该方法接收一个 Runnable 对象,并返回一个 Thread 实例。

下面是 JDK 提供的默认线程工厂 Executors.defaultThreadFactory 的实现源码:

// 定义一个静态内部类 DefaultThreadFactory,实现 ThreadFactory 接口
static class DefaultThreadFactory implements ThreadFactory { 
    // 用于生成线程池编号的静态计数器,保证每个线程池有唯一编号
    private static final AtomicInteger poolNumber = new AtomicInteger(1); 
    
    // 线程组,用于管理线程
    private final ThreadGroup group; 
    
    // 用于生成线程编号的计数器,保证每个线程有唯一编号
    private final AtomicInteger threadNumber = new AtomicInteger(1); 
    
    // 线程名称前缀,用于标识线程池和线程编号
    private final String namePrefix; 

    // 构造函数
    DefaultThreadFactory() { 
        // 获取当前的安全管理器
        SecurityManager s = System.getSecurityManager(); 
        
        // 如果安全管理器存在,则获取其线程组,否则获取当前线程的线程组
        group = (s != null) ? s.getThreadGroup() : 
                              Thread.currentThread().getThreadGroup(); 
        
        // 设置线程名称前缀,格式为 "pool-<线程池编号>-thread-"
        namePrefix = "pool-" + 
                      poolNumber.getAndIncrement() + // 获取并递增线程池编号
                     "-thread-";
    }

    // 实现 ThreadFactory 接口的 newThread 方法,用于创建新线程
    public Thread newThread(Runnable r) { 
        // 创建新线程,指定线程组和要执行的 Runnable 对象,设置线程名称,格式为 "pool-<线程池编号>-thread-<线程编号>"
        Thread t = new Thread(group, r, 
                              namePrefix + threadNumber.getAndIncrement(), 
                              0); // 不设置线程栈大小,使用默认值
        
        // 如果线程是守护线程,将其设置为非守护线程
        if (t.isDaemon()) 
            t.setDaemon(false); 
        
        // 如果线程优先级不是默认的普通优先级,将其优先级设置为普通优先级
        if (t.getPriority() != Thread.NORM_PRIORITY) 
            t.setPriority(Thread.NORM_PRIORITY); 
        
        // 返回创建的线程
        return t; 
    }
}

通过上述源码可以看出,DefaultThreadFactory 创建的线程具有以下特点:

  • 线程名格式为 pool-<线程池编号>-thread-<线程编号>
  • 所有线程都属于同一个线程组。
  • 线程的优先级为普通优先级(Thread.NORM_PRIORITY)。
  • 线程不是守护线程。

通常情况下,使用 JDK 提供的默认线程工厂已经足够满足需求。

六、工作队列

接下来介绍线程池中的 workQueue 参数,它实际上是一个阻塞队列(BlockingQueue)。BlockingQueue 是一个支持阻塞操作的队列接口,提供了线程安全的方式来在队列满或空时进行等待操作。它主要用于生产者-消费者模型:当队列满时,生产者会等待;当队列空时,消费者会等待。

在线程池中,常用的工作队列有以下三种:

  1. 直接交换:SynchronousQueue

    • SynchronousQueue 是一个没有容量的队列。每次 put 操作都会阻塞,直到有消费者执行 take 操作。同样,take 操作也会阻塞,直到有生产者执行 put 操作。
    • 使用 SynchronousQueue 时,建议将 maximumPoolSize 设置得较大,因为没有队列作为缓冲,任务无法排队,线程池可能需要频繁创建新线程来执行任务。
  2. 无界队列:LinkedBlockingQueue

    • LinkedBlockingQueue 是一个无界队列,理论上可以无限存放任务。如果线程池中的线程数达到了 corePoolSize,新任务会被放入队列中,而不会创建新的线程。
    • 这种方式适合处理流量突增的场景,因为任务可以排队等待执行,不会立即消耗额外的 CPU 资源。然而,如果任务提交速度远超处理速度,队列可能会无限增长,最终耗尽内存。
  3. 有界队列:ArrayBlockingQueue

    • ArrayBlockingQueue 是一个有界队列,必须在创建时指定容量。当队列满时,新的任务将触发线程池创建新的线程来处理任务。
    • 使用有界队列时,maximumPoolSize 参数的作用就显现出来了:当队列满了,线程池会尝试创建新线程,直到达到 maximumPoolSize

根据不同的应用场景,可以选择合适的工作队列类型。

七、拒绝策略

关于线程池的拒绝策略,建议参考我写的另一篇博客:拒绝策略详解

至此,线程池参数的各个部分已经介绍完毕。

2.1.2 线程池应该手动创建还是自动创建?

JDK 提供了多种线程池,使用起来非常方便,尤其是自动创建线程池时。然而,尽管自动化在很多场景下是有益的,但对于线程池的创建,手动创建往往会更好。原因如下:

  1. 明确线程池的运行规则:手动创建线程池可以让我们更清楚地控制线程池的行为,避免资源耗尽的风险。
  2. 避免使用默认线程池的潜在问题:JDK 提供的默认线程池在某些情况下可能会引发问题。

接下来,我们详细分析 JDK 提供的默认线程池可能带来的问题。

一、 newFixedThreadPool

首先,我们来看 newFixedThreadPool。通过以下代码示例,我们可以了解该线程池的使用方式:

/**
 * 演示 newFixedThreadPool 的使用
 */
public class FixedThreadPoolTest {
    public static void main(String[] args) {
        // 创建一个包含4个线程的固定大小线程池
        ExecutorService executorService = Executors.newFixedThreadPool(4);
        
        // 提交1000个任务给线程池执行
        for (int i = 0; i < 1000; i++) {
            executorService.execute(new Task());
        }
        
        // 注意:此处没有调用 executorService.shutdown(),程序不会自动结束
    }
}

class Task implements Runnable {
    @Override
    public void run() {
        try {
            // 模拟任务执行时间,线程休眠500毫秒
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 打印当前线程的名称,显示哪个线程在执行任务
        System.out.println(Thread.currentThread().getName());
    }
}

执行结果:
在这里插入图片描述

从控制台输出的线程名称可以看到,线程池中的线程数最多为 4,因为 newFixedThreadPool(4) 创建了一个固定大小为 4 的线程池。即使提交了 1000 个任务,线程池也不会动态扩展线程数量,未执行的任务会被放入阻塞队列中等待。

newFixedThreadPool 的特点

可以用下面这张图来说明newFixedThreadPool线程池的特点:
在这里插入图片描述
这里线程数量是固定的,就是4,我们用这4个线程来反复地执行任务,线程池会不断地轮询,看哪一个线程空闲了,如果线程空闲了,就立马从队列中拿出一个任务,交给该线程执行,如果都没有线程空闲,新来的任务会加入缓存队列,这是一个链表式的队列,容量没有边界。

为什么 newFixedThreadPool 有这样的行为?我们需要从源码层面进行分析。以下是 newFixedThreadPool 的底层实现:

/**
 * 创建一个线程池,该线程池重用固定数量的线程,并从共享的无界队列中获取任务。
 * 在任何时刻,最多只有 nThreads 个线程会处于活跃状态来处理任务。
 * 如果所有线程都在处理任务,而有额外的任务提交,这些任务将会在队列中等待,
 * 直到有线程可用来处理它们。
 * 
 * 线程池中的线程会一直存在,直到显式调用 shutdown 方法关闭线程池。
 *
 * @param nThreads 线程池中的线程数量
 * @return 新创建的线程池
 * @throws IllegalArgumentException 如果 nThreads <= 0,抛出非法参数异常
 */
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

可以看到,newFixedThreadPool 实际上是通过调用 ThreadPoolExecutor 构造函数来创建线程池的。该构造函数接收六个参数,具体如下:

参数名称newFixedThreadPool 传入的值
corePoolSizenThreads
maximumPoolSizenThreads
keepAliveTime0L
workQueuenew LinkedBlockingQueue<Runnable>()
threadFactory默认值(Executors.defaultThreadFactory()
handler默认值(AbortPolicy()

其中,threadFactoryhandler 使用了默认值。接下来,我们进一步分析这两个默认值的含义。

ThreadPoolExecutor 的构造函数中,threadFactoryhandler 的默认值如下:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}

可以看到,threadFactory 使用的是 Executors.defaultThreadFactory(),而 handler 使用的是 defaultHandler。其中,defaultHandler 的具体实现如下:

/**
 * 默认的拒绝执行处理器
 */
private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();

默认的拒绝策略是 AbortPolicy(),即当任务无法提交到线程池时,会抛出 RejectedExecutionException 异常。关于 AbortPolicy 的详细内容,可以参考我的另一篇博客

通过源码分析可以看出,newFixedThreadPool 创建的线程池具有以下特点:

  • 固定线程数量:线程池的核心线程数和最大线程数相同,均为 nThreads。这意味着线程池中的线程数量不会动态扩展,任务会被放入阻塞队列中等待执行。
  • 无界队列:使用 LinkedBlockingQueue 作为任务队列,这意味着任务队列是无界的,可能会导致内存耗尽的风险。
  • 默认拒绝策略:当任务无法被线程池处理时,默认会抛出异常(AbortPolicy)。

因此,虽然 newFixedThreadPool 提供了便捷的线程池创建方式,但在某些场景下,手动创建线程池并指定合适的参数和策略可能会更好,以避免潜在的性能和资源问题。

演示 newFixedThreadPool 导致 OOM(内存溢出)的问题

由于 newFixedThreadPool 使用了无界的 LinkedBlockingQueue 作为任务队列,当请求数持续增加且无法及时处理时,任务会不断堆积在队列中,导致内存占用不断增加,最终可能引发 OutOfMemoryError (OOM)

接下来,我们通过一个实际例子来演示这种情况,代码如下:

/**
 * 演示固定线程池 OOM(内存溢出) 的情况
 * PS:需要调整IDEA的运行参数,增加堆内存大小,以便更快触发OOM
 */
public class FixedThreadPoolOOM {
    // 创建一个固定大小为1的线程池
    private static ExecutorService executorService = Executors.newFixedThreadPool(1);

    public static void main(String[] args) {
        // 无限循环,持续向线程池提交任务
        for (int i = 0;; i++) {
            // 向线程池提交一个新的任务(SubThread)
            executorService.execute(new SubThread());
        }
    }
}

class SubThread implements Runnable {

    @Override
    public void run() {
        try {
            // 这里让线程休眠很长时间,模拟任务执行时间非常长
            // 这样做的目的是为了演示newFixedThreadPool使用无界队列的情况
            // 当线程池中的线程正在执行任务时,后续提交的任务会被放入队列中
            // newFixedThreadPool使用的是LinkedBlockingQueue(无界队列)
            // 如果任务提交速度远远超过任务执行速度,队列会不断增长,最终导致内存耗尽(OOM)
            Thread.sleep(Integer.MAX_VALUE);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

为了更快地观察到 OOM 错误,我们需要将 JVM 的内存限制调小。默认情况下,JVM 分配的内存较大,可能需要较长时间才能触发 OOM。因此,我们需要在运行代码之前,先调整 JVM 的内存参数。

步骤如下:

  1. 运行代码:首先运行 FixedThreadPoolOOM 类,停止运行后,IDEA 的上方下拉框中会出现 FixedThreadPoolOOM 的名称。

  2. 编辑运行配置:点击运行按钮旁边的下拉框,选择 Edit Configurations...

    Edit Configurations

  3. 添加 JVM 参数:在弹出的配置界面中,找到 VM options 选项。如果没有看到该选项,可以点击 Modify options,然后选择 Add VM options,此时就可以看到 VM options 了。

    Add VM Options

  4. 设置内存参数:在 VM options 中填入以下参数:

    -Xmx8m -Xms8m
    

    Set VM Options

    参数解释

    • -Xmx8m:指定 JVM 堆内存的最大值为 8MB。如果程序需要的内存超过这个值,JVM 会抛出 OutOfMemoryError
    • -Xms8m:指定 JVM 堆内存的初始值为 8MB,JVM 启动时会分配这个大小的内存。

配置好 JVM 参数后,运行 FixedThreadPoolOOM 类。随着程序不断向 newFixedThreadPool 线程池提交任务,任务队列中的任务会逐渐堆积,因为线程池中的线程数是固定的,无法及时处理所有任务。由于 LinkedBlockingQueue 是无界队列,任务会无限制地堆积,最终导致内存耗尽,抛出 OutOfMemoryError

控制台输出示例:

OutOfMemoryError

可以看到,程序运行一段时间后,控制台打印了 OutOfMemoryError,说明程序在不断提交任务时,内存被耗尽。

通过这个示例,我们可以看到,newFixedThreadPool 使用的 LinkedBlockingQueue 是无界的。当任务提交速度远超线程池的处理能力时,任务会不断堆积在队列中,导致内存占用持续增加,最终引发 OOM 错误。
总结:

  • newFixedThreadPool 固定了线程池的线程数量,无法根据任务量动态扩展线程数。
  • 由于 LinkedBlockingQueue 是无界队列,任务堆积过多时会占用大量内存,最终导致 OOM。
  • 在实际开发中,使用无界队列时需要特别小心,尤其是在任务量不可控的情况下,建议使用有界队列或其他策略来避免内存耗尽。
二、newSingleThreadExecutor

接下来我们来看一下 JDK 提供的另一种线程池:newSingleThreadExecutor。首先通过代码示例进行演示:

public class SingleThreadPool {
    public static void main(String[] args) {
        // 创建一个单线程的线程池
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        
        // 提交1000个任务到线程池中
        for (int i = 0; i < 1000; i++) {
            // 每个任务执行时,打印当前线程的名字
            // 由于是单线程池,所有任务都会在同一个线程中依次执行
            executorService.execute(() -> System.out.println(Thread.currentThread().getName()));
        }
    }
}

控制台输出结果:

控制台输出

线程池特点分析:

newSingleThreadExecutor 的名字可以看出,它是一个单线程的线程池,意味着线程池中始终只有一个线程在执行任务。通过控制台输出也可以验证,所有任务都是由同一个线程依次执行的。

那么,为什么 newSingleThreadExecutor 会有这样的特点呢?我们可以通过源码来进一步分析。

源码分析:

/**
 * 创建一个使用单个工作线程的 Executor,该线程从一个无界队列中取任务执行。
 * (注意,如果这个单线程在执行过程中由于某种失败而终止,在关闭之前,
 * 如果需要执行后续任务,将会创建一个新的线程来替代它。)
 * 任务保证按顺序执行,并且在任何时刻最多只有一个任务处于活动状态。
 * 与功能类似的 {@code newFixedThreadPool(1)} 不同,返回的执行器保证不会
 * 被重新配置为使用额外的线程。
 *
 * @return 新创建的单线程 Executor
 */
public static ExecutorService newSingleThreadExecutor() {
    // 返回一个 FinalizableDelegatedExecutorService 实例,
    // 该实例封装了一个 ThreadPoolExecutor,配置如下:
    // - 核心线程数为 1
    // - 最大线程数为 1
    // - 空闲线程的存活时间为 0 毫秒(因为只有一个线程,不会有空闲线程)
    // - 使用 LinkedBlockingQueue 作为任务队列(无界队列)
    return new FinalizableDelegatedExecutorService(
        new ThreadPoolExecutor(1, 1, // 核心线程数和最大线程数都为 1,保证只有一个线程
                               0L, TimeUnit.MILLISECONDS, // 空闲线程存活时间为 0 毫秒
                               new LinkedBlockingQueue<Runnable>())); // 使用无界队列
}

从源码可以看出,newSingleThreadExecutor 底层依然是通过 ThreadPoolExecutor 来创建线程池的,只不过它对 ThreadPoolExecutor 进行了封装,传入了特定的参数来保证线程池的行为符合单线程的特点。

值得注意的是,newSingleThreadExecutor 返回的是一个 FinalizableDelegatedExecutorService 对象,它对 ThreadPoolExecutor 进行了包装。这样做的目的是为了隐藏某些不必要的功能,防止用户通过修改线程池的配置(例如增加线程数)来破坏单线程执行的初衷。

参数分析:

我们可以通过下表总结 newSingleThreadExecutor 创建线程池时传入的参数:

参数名称newSingleThreadExecutor 传入的值
corePoolSize1
maximumPoolSize1
keepAliveTime0L
workQueuenew LinkedBlockingQueue<Runnable>()
threadFactory默认值(Executors.defaultThreadFactory()
handler默认值(AbortPolicy()

从表格中可以看出,newSingleThreadExecutor 的核心线程数和最大线程数都被设置为 1,保证了线程池中始终只有一个线程在执行任务。任务队列使用的是 LinkedBlockingQueue,这是一个无界队列,意味着如果任务提交的速度超过了线程执行的速度,任务会不断堆积,可能会导致内存溢出(OOM)。

总结:

  • newSingleThreadExecutor 创建的线程池中始终只有一个线程,所有任务都会按照提交的顺序依次执行。
  • 如果线程在执行过程中意外终止,线程池会创建一个新的线程来继续执行后续任务,保证任务不会中断。
  • 由于使用了无界队列,如果任务提交过多且处理速度跟不上,可能会导致内存溢出(OOM)。
  • newSingleThreadExecutor 底层依然是通过 ThreadPoolExecutor 实现的,但通过 FinalizableDelegatedExecutorService 进行了封装,防止用户修改线程池的配置,确保其单线程的特性。

newFixedThreadPool(1) 类似,newSingleThreadExecutor 也存在任务堆积导致 OOM 的风险,因此在使用时需要注意任务提交的速度和数量。

三、newCachedThreadPool 线程池

接下来,我们来看 JDK 提供的第三种线程池——newCachedThreadPool。这种线程池的特点是可以缓存线程。那么,什么叫“可以缓存”呢?首先,它使用的是无界队列,并且可以自动回收多余的线程。我们通过下图来说明 newCachedThreadPool 线程池的特点:

在这里插入图片描述

newCachedThreadPool 使用的是 SynchronousQueue 作为任务队列。SynchronousQueue 是一种特殊的队列,它的容量为零,任务不会在队列中排队,而是直接交给线程执行。其工作机制是:只有当消费者线程准备好消费任务时,生产者线程才能提交任务,反之亦然。因此,任务一旦提交,就必须立即被某个线程处理。

如果当前没有空闲线程可用,newCachedThreadPool 会创建一个新的线程来执行任务。由于 newCachedThreadPool 的最大线程数被设置为 Integer.MAX_VALUE,理论上它可以创建无限多的线程来处理任务。因此,无论有多少任务提交,线程池都会创建足够的线程来执行这些任务。

“缓存”指的是线程的缓存机制。当任务高峰期过后,线程池中不再需要那么多线程来处理任务,空闲的线程如果超过 60 秒没有被使用,就会被自动销毁。这种机制可以有效避免资源浪费。

下面通过代码演示 newCachedThreadPool 的使用:

public class CachedThreadPool {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 1000; i++) {
            executorService.execute(() -> System.out.println(Thread.currentThread().getName()));
        }
    }
}

输出结果如下:

在这里插入图片描述

从输出可以看到,控制台打印了大量不同线程的名称,说明 newCachedThreadPool 确实会根据任务数量动态创建大量线程来执行任务。当任务执行完毕后,线程池中的空闲线程会在一段时间后被销毁。

我们从源码的角度来看,为什么 newCachedThreadPool 会有这些特点:

/**
 * 创建一个线程池,该线程池会根据需要创建新线程,但在可用时会重用之前构造的线程。
 * 这种线程池通常可以提高执行许多短生命周期异步任务的程序的性能。
 * 调用 {@code execute} 方法时,如果有可用的线程,则会重用之前构造的线程。
 * 如果没有可用的线程,则会创建一个新线程并将其添加到线程池中。
 * 没有被使用的线程在60秒后会被终止并从缓存中移除。因此,线程池如果长时间处于空闲状态,
 * 将不会消耗任何资源。需要注意的是,具有类似属性但不同细节(例如超时参数)的线程池
 * 可以通过 {@link ThreadPoolExecutor} 构造函数创建。
 *
 * @return 新创建的线程池
 */
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE, // 核心线程数为 0,最大线程数为 Integer.MAX_VALUE
                                  60L, TimeUnit.SECONDS, // 空闲线程存活时间为 60 秒
                                  new SynchronousQueue<Runnable>()); // 使用 SynchronousQueue 作为任务队列
}

从源码可以看出,newCachedThreadPool 的线程池配置如下:

  • 核心线程数 (corePoolSize):0,表示没有核心线程,所有线程都是非核心线程。
  • 最大线程数 (maximumPoolSize)Integer.MAX_VALUE,理论上可以创建无限多的线程。
  • 空闲线程存活时间 (keepAliveTime):60 秒,空闲线程超过 60 秒未被使用将被销毁。
  • 任务队列 (workQueue)SynchronousQueue,任务不会排队,直接交给线程执行。

我们可以通过下表总结 newCachedThreadPool 在构造线程池时传入的参数:

参数名称newCachedThreadPool 传入的值
corePoolSize0
maximumPoolSizeInteger.MAX_VALUE
keepAliveTime60L
workQueuenew SynchronousQueue<Runnable>()
threadFactory默认值(Executors.defaultThreadFactory()
handler默认值(AbortPolicy()

虽然 newCachedThreadPool 具有动态扩展线程数的优势,但它也存在潜在的问题。由于最大线程数被设置为 Integer.MAX_VALUE,如果任务数量非常多,线程池会创建大量线程,可能导致内存溢出(OOM,Out of Memory)。因此,在使用 newCachedThreadPool 时,需要特别注意任务的数量和系统的资源限制。

newCachedThreadPool 适用于执行大量短期异步任务的场景,能够根据任务量动态调整线程数,且在任务执行完毕后自动回收空闲线程。然而,由于其线程数没有上限,使用时需要注意避免因过多线程导致的资源耗尽问题。

四、newScheduledThreadPool

接下来,我们来看 JDK 提供的第四种线程池——newScheduledThreadPool。从名称可以看出,Scheduled 意味着“预先安排的”或“按时刻表的”,这表明该线程池支持定时和周期性执行任务的功能。

我们通过代码来演示 newScheduledThreadPool 的使用:

public class ScheduledThreadPool {
    public static void main(String[] args) {
        // 创建一个具有10个线程的定时线程池
        ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(10);
        
        // 调度一个任务,延迟5秒后执行
        threadPool.schedule(new Task(), 5, TimeUnit.SECONDS);
    }
}

class Task implements Runnable {
    @Override
    public void run() {
        try {
            // 模拟任务执行时的延迟,线程休眠500毫秒
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 打印当前线程的名称
        System.out.println(Thread.currentThread().getName());
    }
}

在这个例子中,我们创建了一个具有 10 个线程的定时线程池,并使用 schedule 方法提交了一个任务。该任务将在延迟 5 秒后执行。运行程序时,最初控制台没有输出,5 秒后才会打印线程名称,说明任务被延迟执行。

除了 schedule 方法,newScheduledThreadPool 还支持周期性执行任务。我们可以使用 scheduleAtFixedRate 方法来让任务按照固定的频率重复执行。代码如下:

public class ScheduledThreadPool {
    public static void main(String[] args) {
        // 创建一个具有10个线程的定时线程池
        ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(10);
        
        // 调度一个任务,初始延迟1秒后开始执行,之后每隔3秒重复执行一次
        threadPool.scheduleAtFixedRate(new Task(), 1, 3, TimeUnit.SECONDS);
    }
}

在这个例子中,任务会在初始延迟 1 秒后开始执行,之后每隔 3 秒重复执行一次。运行程序时,控制台会在 1 秒后输出当前执行任务的线程名称,然后每隔 3 秒重复输出一次。

newScheduledThreadPool 的主要特点是支持定时和周期性任务调度。它可以根据我们的需求,按指定的时间延迟或以固定的频率执行任务。这使得它非常适合用于需要定时执行任务的场景,例如定时备份、定时任务调度等。

五、总结

线程池的创建方式有自动和手动两种。虽然 JDK 提供了 Executors 工具类来简化线程池的创建,但我们在实际开发中,最好手动创建线程池,原因如下:

  1. 业务需求的差异:自动创建的线程池(如 newFixedThreadPoolnewCachedThreadPool 等)是根据通用场景设计的,无法完全契合我们具体的业务需求。手动创建线程池可以根据业务场景灵活设置线程池的参数。

  2. 自定义线程工厂:通过自定义线程工厂,我们可以为线程设置有意义的名称,方便后续的调试和日志分析。

  3. 自定义拒绝策略:当任务被拒绝时,默认的拒绝策略可能不适合我们的业务需求。通过自定义拒绝策略,我们可以记录合适的日志或采取其他措施,方便后续排查问题。

  4. 线程数量的调优:线程池的线程数量应该根据业务的并发量来合理设置。通过手动创建线程池,我们可以根据实际的业务场景和性能调研结果,合理设置线程池的核心线程数、最大线程数、队列容量等参数。

2.1.3 线程池的数量设置为多少合适?

在使用线程池时,如何合理设置线程数量是一个关键问题。假设我们有1000个任务,那么应该使用多少个线程来执行这些任务呢?这里主要关注核心线程的数量,该数量通过 corePoolSize 参数控制。

线程池的核心线程数量并不是固定的,它需要根据具体的任务类型进行调整。常见的任务类型主要分为 CPU密集型IO密集型,两者的最佳线程数量设置有显著差异。

  1. CPU密集型任务(如加密、计算等)

对于CPU密集型任务,最佳的线程数量通常为 CPU核心数的1-2倍。这是因为CPU密集型任务会让CPU满负荷运行,线程数量过多反而会导致频繁的线程切换,降低效率。因此,线程数量应设置为接近CPU核心数,甚至稍微多一点。

例如,如果机器是8核CPU,那么核心线程数量可以设置为 8到16 之间。这样可以让每个CPU核心专注于执行某个线程,避免频繁的上下文切换。

  1. IO密集型任务(如读写数据库、文件、网络等)

对于IO密集型任务,最佳的线程数量通常是 CPU核心数的多倍。这是因为IO操作(如文件读写、网络请求等)通常比CPU处理速度慢得多,CPU在大部分时间处于空闲状态,等待IO操作完成。因此,可以通过增加线程数量来充分利用CPU资源。

例如,如果机器是8核CPU,对于IO密集型任务,线程数量可以设置为 CPU核心数的10倍,即 80个线程 或更多。虽然线程数量看起来很多,但大部分线程都在等待IO操作完成,CPU只需在IO操作结束后进行短暂的处理,然后继续等待下一次IO操作。

为了更精确地确定线程数量,可以参考 Brian Goetz 推荐的计算公式:

线程数 = CPU核心数 × ( 1 + 平均等待时间 平均工作时间 ) \text{线程数} = \text{CPU核心数} \times \left(1 + \frac{\text{平均等待时间}}{\text{平均工作时间}}\right) 线程数=CPU核心数×(1+平均工作时间平均等待时间)

  • 如果任务的 平均工作时间 较长,那么公式中的系数较小,线程数量也会较少。
  • 如果任务的 平均等待时间 较长,则系数较大,线程数量也会相应增加。

假设机器的CPU核心数为8,任务是读取数据操作,平均等待时间为10秒,平均工作时间为1秒。根据公式:

线程数 = 8 × ( 1 + 10 1 ) = 8 × 11 = 88 \text{线程数} = 8 \times \left(1 + \frac{10}{1}\right) = 8 \times 11 = 88 线程数=8×(1+110)=8×11=88

因此,核心线程数量可以设置为 88

需要注意的是,上述公式只是一个估算工具,实际的线程数量设置还需要通过 性能压测 来验证。通过压测,可以更准确地找到适合具体业务场景的线程数量。

总结:

  • CPU密集型任务:线程数量应设置为 CPU核心数的1-2倍
  • IO密集型任务:线程数量应设置为 CPU核心数的多倍,具体倍数根据任务的IO等待时间和工作时间来决定。
  • 计算公式:可以使用 线程数 = CPU核心数 × ( 1 + 平均等待时间 / 平均工作时间 ) \text{线程数} = \text{CPU核心数} \times (1 + 平均等待时间/ 平均工作时间) 线程数=CPU核心数×(1+平均等待时间/平均工作时间)进行估算。
  • 压测验证:最终的线程数量应通过压测来确定,以确保系统在实际运行中的性能最优。

2.2 停止线程池的正确姿势

这里和线程的停止不一样。线程是创建容易,停止难,因为要考虑的东西很多,线程池正好反过来,创建难,停止容易。

3 常见线程池的特点和用法

在前面已经演示了JDK提供的默认线程池的使用,接下来我们将整理一下JDK提供的线程池相关类及它们的关系。

3.1 线程池类关系图

下图展示了线程池相关类之间的关系:

线程池类关系图

其中,Executors 是一个工具类,提供了多种预定义参数的线程池,方便直接创建使用。

3.2 线程池参数对比表

下表总结了JDK提供的四种常见线程池的参数和特点:

参数名称newFixedThreadPool 传入的值newSingleThreadExecutor 传入的值newCachedThreadPool 传入的值newScheduledThreadPool 传入的值
corePoolSizenThreads10corePoolSize
maximumPoolSizenThreads1Integer.MAX_VALUEInteger.MAX_VALUE
keepAliveTime0L0L60L0L
workQueuenew LinkedBlockingQueue<Runnable>()new LinkedBlockingQueue<Runnable>()new SynchronousQueue<Runnable>()new DelayedWorkQueue()
threadFactory默认值(Executors.defaultThreadFactory()默认值(Executors.defaultThreadFactory()默认值(Executors.defaultThreadFactory()默认值(Executors.defaultThreadFactory()
handler默认值(AbortPolicy()默认值(AbortPolicy()默认值(AbortPolicy()默认值(AbortPolicy()
特点线程数量是固定的线程数量固定且只有1个缓存线程、自动回收多余线程支持定时以及周期性执行任务

3.3 工作队列分析

接下来分析这些线程池所使用的工作队列(阻塞队列):

  1. newFixedThreadPoolnewSingleThreadExecutor 使用的是 LinkedBlockingQueue

    • LinkedBlockingQueue 是基于链表实现的无界队列,即队列没有容量限制,永远不会满。当线程数量达到 corePoolSize 后,新的任务会进入队列等待执行,而不会创建新的线程。这与这两种线程池的特点相符,即线程数量是固定的。
  2. newCachedThreadPool 使用的是 SynchronousQueue

    • SynchronousQueue 是一个没有存储空间的队列,每个插入操作必须等待相应的移除操作。也就是说,任务来了必须有线程立即执行,否则任务无法提交。这种设计与 newCachedThreadPool 的特点相符:线程池的最大线程数是 Integer.MAX_VALUE,可以创建大量线程来处理任务,而 SynchronousQueue 的阻塞时间也不会过长。
  3. newScheduledThreadPool 使用的是 DelayedWorkQueue

    • DelayedWorkQueue 是一个支持延迟执行的队列,任务可以按照设定的时间延迟执行,符合 newScheduledThreadPool 支持定时和周期性任务执行的特点。

3.4 JDK 1.8 新增的线程池

workStealingPool 是 JDK 1.8 新增的线程池,与前面几种线程池有较大不同。它的主要特点如下:

  1. 任务可以产生子任务
    适合使用 workStealingPool 的任务通常是可以产生子任务的。例如,树的遍历或矩阵处理任务。在遍历二叉树时,每个节点的子节点可以看作是一个子任务;在处理矩阵时,可以将矩阵分割成多个小矩阵,每个小矩阵的处理也可以看作是子任务。

  2. 任务窃取机制
    workStealingPool 中的每个线程都有自己的任务队列。当某个线程产生了子任务时,这些子任务会放入该线程的任务队列中执行。如果其他线程空闲,它们可以从这个线程的任务队列中窃取任务来执行。这种机制提高了任务的并行执行效率。

  3. 注意事项

    • 任务不应加锁:为了实现并行执行,任务最好不要加锁。如果任务加锁,多个线程同时执行时会导致并行性下降。
    • 不保证执行顺序:与其他线程池不同,workStealingPool 不保证任务的执行顺序。由于任务被拆分为多个子任务,并且子任务可能被不同的线程并行执行,因此执行顺序无法保证。例如,在树的遍历中,期望的遍历顺序可能是 1-2-3-4-5,但实际执行结果可能是 1-3-4-2-5
  4. 适用场景
    workStealingPool 适用于递归任务或可以产生子任务的场景。由于这种任务在日常开发中相对较少,因此 workStealingPool 的使用场景有限。开发者可以在遇到合适的场景时深入研究和使用该线程池。

3.5 停止线程池的正确方法

在多线程编程中,正确地停止线程池是非常重要的。下面我们将介绍与停止线程池相关的五个方法,并逐一进行讲解。

3.5.1 shutdown

shutdown 方法是最简单、直观的停止线程池的方法。从字面上看,它的作用是关闭线程池,但实际上调用该方法后,线程池并不会立即停止。shutdown 方法只是启动了线程池的关闭过程。

当调用 shutdown 方法时,线程池会停止接收新的任务,但会继续执行已经提交的任务,包括正在执行的任务和任务队列中的任务。只有当所有任务执行完毕后,线程池才会真正关闭。因此,shutdown 方法的关闭过程是有序且优雅的。

调用 shutdown 后,如果再尝试提交新任务,线程池会拒绝这些任务,并抛出 RejectedExecutionException 异常。

下面是一个代码示例,演示了 shutdown 方法的使用:

/**
 * 演示关闭线程池
 */
public class ShutDown {
    public static void main(String[] args) throws InterruptedException {
        // 创建一个固定大小为10的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        
        // 提交1000个任务到线程池中执行
        for (int i = 0; i < 1000; i++) {
            executorService.execute(new ShutDownTask());
        }
        
        // 主线程休眠1.5秒,等待部分任务执行
        Thread.sleep(1500);
        
        // 调用shutdown方法,开始有序关闭线程池,已提交的任务会继续执行,但不再接受新任务
        executorService.shutdown();
        
        // 尝试在线程池关闭后提交新任务,这会抛出RejectedExecutionException异常
        executorService.execute(new ShutDownTask());
    }
}

class ShutDownTask implements Runnable {
    @Override
    public void run() {
        try {
            // 任务执行时休眠500毫秒,模拟任务处理
            Thread.sleep(500);
            // 打印当前线程的名称
            System.out.println(Thread.currentThread().getName());
        } catch (InterruptedException e) {
            // 捕获并打印中断异常
            e.printStackTrace();
        }
    }
}

在执行 shutdown 方法后,线程池不再接受新任务,且如果尝试提交新任务,会抛出 RejectedExecutionException 异常。newFixedThreadPool 使用的是默认的拒绝策略,即中断策略(AbortPolicy),直接抛出异常。

3.5.2 isShutdown

isShutdown 方法用于判断线程池是否已经开始关闭。调用 shutdown 方法后,线程池会进入关闭状态,但这并不意味着线程池已经完全停止。isShutdown 方法返回一个布尔值,true 表示线程池已经开始关闭,false 表示线程池仍在运行。

在上面的代码基础上,我们可以通过 isShutdown 方法来判断线程池是否已经开始关闭:

System.out.println(executorService.isShutdown());  // 打印 false
executorService.shutdown(); 
System.out.println(executorService.isShutdown());  // 打印 true

控制台输出如下:

isShutdown输出

3.5.3 isTerminated

isShutdown 方法只能判断线程池是否开始关闭,但并不能判断线程池是否已经完全停止。要判断线程池是否已经完全停止,需要使用 isTerminated 方法。

isTerminated 方法返回一个布尔值,true 表示线程池已经完全停止,false 表示线程池中仍有任务在执行或等待执行。

示例代码如下:

executorService.shutdown();
System.out.println(executorService.isShutdown());    // 打印 true
System.out.println(executorService.isTerminated());  // 打印 false

输出如下:

isTerminated输出

调用 shutdown 方法后,立即调用 isTerminated 方法,结果为 false,表示线程池还没有完全停止。此时,线程池不再接收新任务,但队列中的任务和正在执行的任务还未完成。只有当所有任务执行完毕后,isTerminated 方法才会返回 true

3.5.4 awaitTermination

awaitTermination 方法并不是用来直接停止线程池的,而是用于阻塞调用该方法的线程,直到线程池完全停止或等待的时间到达。它的典型用法如下:

/**
 * 演示关闭线程池
 */
public class ShutDown {
    public static void main(String[] args) throws InterruptedException {
        // 创建一个具有固定大小为10的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        
        // 提交10个任务到线程池中执行
        for (int i = 0; i < 10; i++) {
            executorService.execute(new ShutDownTask());
        }
       
        // 调用shutdown方法,开始有序关闭线程池,已提交的任务会继续执行,但不再接受新任务
        executorService.shutdown();
        
        // 等待线程池中的任务在指定时间内完成,最长等待1000秒
        // 如果所有任务在此时间内完成,则返回true,否则返回false
        executorService.awaitTermination(1000L, TimeUnit.SECONDS);
        
        // 打印线程池是否已经终止,应该输出 true
        System.out.println(executorService.isTerminated()); // 打印 true
    }
}

在这个例子中,awaitTermination 方法会阻塞主线程,直到线程池中的任务全部完成或等待时间(1000秒)到达。由于等待时间设置得较长,最终线程池会完全停止,输出 true

如果等待时间较短,线程池可能还未完全停止,awaitTermination 会返回 false。示例如下:

/**
 * 演示关闭线程池
 */
public class ShutDown {
    public static void main(String[] args) throws InterruptedException {
        // 创建一个具有固定大小为10的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        
        // 提交10个任务到线程池中执行
        for (int i = 0; i < 10; i++) {
            executorService.execute(new ShutDownTask());
        }
        
        // 调用shutdown方法,开始有序关闭线程池,已提交的任务会继续执行,但不再接受新任务
        executorService.shutdown();
        
        // 等待线程池中的任务在指定时间内完成,这里等待1毫秒
        // 由于时间非常短,几乎不可能所有任务都完成,因此awaitTermination会立即返回false
        executorService.awaitTermination(1L, TimeUnit.MILLISECONDS);
        
        // 打印线程池是否已经终止,应该输出 false,因为任务尚未全部完成
        System.out.println(executorService.isTerminated()); // 打印 false
    }
}

控制台输出如下:

awaitTermination输出

需要注意的是,调用 awaitTermination 方法的线程会被阻塞,直到线程池完全停止或等待时间到达。如果在此期间线程被中断,awaitTermination 会抛出 InterruptedException 异常。

3.5.5 shutdownNow

shutdownNow 是一种强制停止线程池的方法,和 shutdown 方法不同,它会立即尝试停止线程池。其行为包括:

  • 拒绝接收新的任务。
  • 放弃任务队列中的任务,并将这些任务作为 shutdownNow 方法的返回值。
  • 调用每个线程的 interrupt 方法,向线程发送中断信号,具体的中断逻辑由每个线程自行处理。

线程可以通过 isInterrupted 方法检测中断信号。下面是 shutdownNow 的使用示例:

/**
 * 演示强制关闭线程池
 */
public class ShutDown {
    public static void main(String[] args) throws InterruptedException {
        // 创建一个固定大小为10的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        
        // 提交1000个任务给线程池执行
        for (int i = 0; i < 1000; i++) {
            executorService.execute(new ShutDownTask());
        }
        
        // 立即关闭线程池,并返回未执行的任务列表
        List<Runnable> runnables = executorService.shutdownNow();
        
        // 打印线程池是否已经关闭
        System.out.println(executorService.isShutdown()); 
        
        // 打印线程池中的任务是否全部完成
        System.out.println(executorService.isTerminated());
    }
}

class ShutDownTask implements Runnable {
    @Override
    public void run() {
        try {
            // 模拟任务执行,通过让线程休眠500毫秒
            Thread.sleep(500);
            // 打印当前线程的名称,表示任务由哪个线程执行
            System.out.println(Thread.currentThread().getName());
        } catch (InterruptedException e) {
            // 如果线程在休眠时被中断,捕获中断异常并打印提示信息
            System.out.println(Thread.currentThread().getName() + "被中断了!");
        }
    }
}

输出结果如下:

shutdownNow输出

可以看到,shutdownNow 方法会立即停止线程池,未执行的任务会被返回,并且正在执行的任务会收到中断信号。开发者可以根据返回的任务列表,决定是否将这些任务交给其他线程池执行,或者记录日志。

3.5.6 总结

以下是五个与线程池停止相关的方法的总结:

方法名描述
shutdown有序关闭线程池,已提交的任务会继续执行,但不再接受新任务。
isShutdown判断线程池是否已经开始关闭,返回 true 表示线程池已开始关闭,但不一定完全停止。
isTerminated判断线程池是否已经完全停止,返回 true 表示线程池中所有任务都已执行完毕,线程池已完全停止。
awaitTermination阻塞调用该方法的线程,直到线程池完全停止或等待时间到达。返回 true 表示线程池已完全停止,false 表示超时。
shutdownNow立即停止线程池,拒绝新任务,放弃队列中的任务,并中断正在执行的任务。返回未执行的任务列表。

通过这些方法,开发者可以根据具体需求优雅地控制线程池的关闭过程,确保任务执行的有序性和安全性。

4 任务太多,怎么拒绝?

拒绝策略可以参考我写的另外一篇博客
在这里插入图片描述

5 钩子方法,给线程池加点料

在这一节中,我们将介绍线程池的钩子方法。线程池本身非常强大,前面我们已经展示了一些常见的使用方法。除了这些常规方法,线程池还提供了钩子方法,允许我们在每个任务执行前后进行一些额外的操作,比如记录日志、统计数据等。

接下来,我们通过代码演示如何自定义一个可暂停的线程池PauseableThreadPool)。顾名思义,这个线程池具备暂停和恢复的功能。

5.1 继承 ThreadPoolExecutor

首先,我们让 PauseableThreadPool 继承 ThreadPoolExecutor。由于 ThreadPoolExecutor 没有无参构造函数,因此我们需要在 PauseableThreadPool 的构造函数中显式调用父类的构造方法。我们可以通过 IDE 自动生成这些构造方法。

插图

这个类的主要功能是展示如何在每个任务执行前后插入钩子函数。钩子函数在设计模式中也经常被使用。

5.2 实现暂停功能

我们首先定义一个 isPaused 变量,并使用 ReentrantLock 来实现对该变量的并发访问和修改。

// 标识线程池是否处于暂停状态
private boolean isPaused;
// 可重入锁,用于控制线程池的暂停和恢复操作
private final ReentrantLock lock = new ReentrantLock();
// 条件变量,用于在暂停时阻塞线程,恢复时唤醒线程
private Condition condition = lock.newCondition();
  • isPaused:用于判断线程池是否处于暂停状态。
  • lock:用于并发控制,确保对 isPaused 的修改是线程安全的。
  • condition:用于在线程池暂停时阻塞线程,并在恢复时唤醒它们。

5.2.1 暂停方法

pause 方法的实现如下。多个线程竞争锁,获取到锁的线程修改 isPaused 变量,避免并发修改导致线程安全问题。

// 暂停线程池的执行
private void pause() {
    lock.lock(); // 获取锁
    try {
        isPaused = true; // 设置线程池为暂停状态
    } finally {
        lock.unlock(); // 释放锁
    }
}

5.2.2 恢复方法

resume 方法的实现如下:

// 恢复线程池的执行
private void resume() {
    lock.lock(); // 获取锁
    try {
        isPaused = false; // 设置线程池为非暂停状态
        condition.signalAll(); // 唤醒所有等待的线程
    } finally {
        lock.unlock(); // 释放锁
    }
}

5.3 钩子方法

ThreadPoolExecutor 提供了三个钩子方法:

  • beforeExecute:在任务执行前调用。
  • afterExecute:在任务执行后调用。
  • terminated:在线程池终止时调用。

我们通过重写 beforeExecute 方法来实现线程池的暂停功能。

// 在每个任务执行前调用,用于检查线程池是否处于暂停状态
@Override
protected void beforeExecute(Thread t, Runnable r) {
    super.beforeExecute(t, r); // 调用父类的 beforeExecute 方法
    lock.lock(); // 获取锁
    try {
        // 如果线程池处于暂停状态,当前线程等待
        while (isPaused) {
            condition.await(); // 线程等待,直到被唤醒
        }
    } catch (InterruptedException e) {
        e.printStackTrace(); // 捕获中断异常
    } finally {
        lock.unlock(); // 释放锁
    }
}

在每个任务执行之前,beforeExecute 方法会被调用。我们首先获取锁,然后检查 isPaused 变量。如果线程池处于暂停状态,当前线程将进入等待状态,直到被唤醒。

插图

5.4 完整代码

public class PauseableThreadPool extends ThreadPoolExecutor {
    // 标识线程池是否处于暂停状态
    private boolean isPaused;
    // 可重入锁,用于控制线程池的暂停和恢复操作
    private final ReentrantLock lock = new ReentrantLock();
    // 条件变量,用于在暂停时阻塞线程,恢复时唤醒线程
    private Condition condition = lock.newCondition();

    // 构造方法1:初始化线程池,指定核心线程数、最大线程数、线程存活时间、时间单位和任务队列
    public PauseableThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }

    // 构造方法2:额外指定线程工厂
    public PauseableThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
    }

    // 构造方法3:额外指定拒绝策略
    public PauseableThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
    }

    // 构造方法4:同时指定线程工厂和拒绝策略
    public PauseableThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
    }

    // 暂停线程池的执行
    private void pause() {
        lock.lock(); // 获取锁
        try {
            isPaused = true; // 设置线程池为暂停状态
        } finally {
            lock.unlock(); // 释放锁
        }
    }

    // 恢复线程池的执行
    private void resume() {
        lock.lock(); // 获取锁
        try {
            isPaused = false; // 设置线程池为非暂停状态
            condition.signalAll(); // 唤醒所有等待的线程
        } finally {
            lock.unlock(); // 释放锁
        }
    }

    // 在每个任务执行前调用,用于检查线程池是否处于暂停状态
    @Override
    protected void beforeExecute(Thread t, Runnable r) {
        super.beforeExecute(t, r); // 调用父类的 beforeExecute 方法
        lock.lock(); // 获取锁
        try {
            // 如果线程池处于暂停状态,当前线程等待
            while (isPaused) {
                condition.await(); // 线程等待,直到被唤醒
            }
        } catch (InterruptedException e) {
            e.printStackTrace(); // 捕获中断异常
        } finally {
            lock.unlock(); // 释放锁
        }
    }

    // 主方法,测试线程池的暂停和恢复功能
    public static void main(String[] args) throws InterruptedException {
        // 创建一个 PauseableThreadPool 实例,核心线程数和最大线程数为10,线程存活时间为10秒
        PauseableThreadPool pauseableThreadPool = new PauseableThreadPool(10,
                10, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
        
        // 提交1000个任务到线程池
        for (int i = 0; i < 1000; i++) {
            pauseableThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    // 每个任务打印当前线程名称并休眠10毫秒
                    System.out.println(Thread.currentThread().getName() + " 我被执行");
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }

        // 主线程休眠1.5秒后暂停线程池
        Thread.sleep(1500);
        pauseableThreadPool.pause();
        System.out.println("线程池被暂停了");

        // 主线程再休眠1.5秒后恢复线程池
        Thread.sleep(1500);
        pauseableThreadPool.resume();
        System.out.println("线程池恢复了");
    }
}

5.5 控制台输出

插图

5.6 总结

通过重写 ThreadPoolExecutor 的钩子方法,我们可以在任务执行前后插入自定义逻辑。在本例中,我们实现了一个可暂停的线程池,并通过 beforeExecute 方法控制任务的执行状态。

6 实现原理、源码分析

这一节,我们将对线程池的实现原理,包括源码进行一下分析。

6.1 线程池组成部分

首先来看线程池是由哪几部分组成,实际上最重要的是由这四个部分组成:

  • 线程管理器:包括创建线程池,停止线程池,这些是主要管理线程池用到。
  • 工作线程:创建出来用于执行任务的线程,会反复地去任务队列里面执行任务。
  • 任务队列:用于存放任务的队列。
  • 任务接口(Task):我们一个一个执行的任务。

6.2 Executor家族

线程池、ThreadPoolExecutor、ExecutorService、Executor、Executors这么多和线程池相关的类,大家都是什么关系?

6.2.1 Executor

Executor是一个顶层接口,我们来看一下源码:

public interface Executor {
    void execute(Runnable command);
}

Executor 接口只有一个execute方法,线程池需要实现该方法,提交任务。

6.2.2 ExecutorService

ExecutorService 也是一个接口,继承于Executor,但是ExecutorService提供的方法就更多了

public interface ExecutorService extends Executor {

    void shutdown();

    List<Runnable> shutdownNow();

    boolean isShutdown();

    boolean awaitTermination(long timeout, TimeUnit unit)
        throws InterruptedException;

    <T> Future<T> submit(Callable<T> task);

    Future<?> submit(Runnable task);

  
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
        throws InterruptedException;

    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
                                  long timeout, TimeUnit unit)
        throws InterruptedException;

    <T> T invokeAny(Collection<? extends Callable<T>> tasks,
                    long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

ExecutorService 接口提供了前面讲到的与线程池关闭相关的五个方法,以及提交任务的sumbit方法,还有invokeAny方法可以得到线程的返回结果。

6.2.3 Executors

Executors是一个工具类,该工具类提供了很多工具方法,用于创建各种各样的线程池,上文介绍到的。

6.2.4 ThreadPoolExecutor

ThreadPoolExecutor类就是真正意义上的线程池,ThreadPoolExecutor类的构造函数有很多,我们通过不同的参数来设置以满足我们的 业务需求。

6.3 线程池实现线程复用的原理

线程池实现线程复用的核心原理在于同一线程可以执行不同的任务,而无需频繁创建和销毁线程。通过对线程的封装,线程池避免了线程的重复启动。通常,线程的启动是通过调用 start 方法来实现的,但线程池只需启动线程一次,之后可以直接处理新的任务。

线程池中的线程并不是简单地执行 run 方法,而是通过一个循环不断检查是否有新任务到来。一旦有新任务,线程池会调用该任务的 run 方法来执行任务。线程池中的线程会从任务队列中取出任务并执行,从而实现线程的复用。

接下来,我们通过源码来详细解析线程池是如何实现线程复用的。首先,提交一个任务到线程池的代码如下:

threadpool.execute(r);

进入 execute 方法,可以看到如下代码:

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
        
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (!isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    } else if (!addWorker(command, false)) {
        reject(command);
    }
}

这段代码首先检查提交的任务是否为空,如果为空则抛出 NullPointerException。接着,它会判断当前线程池中的线程数量是否小于核心线程数。如果是,则调用 addWorker 方法来添加核心线程。

addWorker 方法

addWorker 方法中,会创建一个 Worker 对象:

w = new Worker(firstTask);

Worker 类是线程池中的核心类,负责实际的任务执行。进入 Worker 类后,我们可以看到 runWorker 方法:

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();
            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);
                } finally {
                    afterExecute(task, thrown);
                }
            } finally {
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly);
    }
}

在这段代码中,task.run() 是任务执行的核心部分。Worker 类中的线程会不断从任务队列中获取任务并执行,直到没有任务可执行。Worker 对象的生命周期内只会调用一次 start 方法。

execute 方法的详细解析

回到 execute 方法,接下来会判断线程池的状态以及任务队列的情况:

if (isRunning(c) && workQueue.offer(command)) {
    int recheck = ctl.get();
    if (!isRunning(recheck) && remove(command))
        reject(command);
    else if (workerCountOf(recheck) == 0)
        addWorker(null, false);
}

这里首先判断线程池是否处于 RUNNING 状态。如果线程池正在运行且任务队列可以接受新任务,则将任务放入队列中。然后再次检查线程池的状态,如果线程池已经不再运行,则从队列中移除任务并拒绝执行。如果线程池中没有线程,则需要添加一个新的线程来处理任务。

如果任务无法放入队列,或者线程池不处于 RUNNING 状态,则会尝试添加一个非核心线程来执行任务。如果添加失败,则会拒绝该任务:

else if (!addWorker(command, false)) {
    reject(command);
}

线程池的状态

线程池有五种状态:

  1. RUNNING:接收新任务并处理队列中的任务。
  2. SHUTDOWN:不再接收新任务,但会处理队列中的任务。
  3. STOP:不接收新任务,也不处理队列中的任务,并中断正在执行的任务。
  4. TIDYING:所有任务都已终止,workerCount 为 0 时,线程池会进入该状态,并执行 terminated 钩子方法。
  5. TERMINATEDterminated 方法执行完毕,线程池完全终止。

线程池的执行流程图

线程池的 execute 方法的整体流程如下图所示:

线程池执行流程图

7. 使用线程池的注意事项

在使用线程池时,需要注意以下几点:

  1. 避免任务堆积:任务堆积会导致线程池中的任务队列过长,可能会耗尽系统资源。
  2. 避免线程数过度增加:线程数过多会导致系统资源紧张,影响性能。
  3. 排查线程泄漏:线程泄漏是指线程执行完任务后未能正确回收,尤其是非核心线程在空闲一段时间后无法被回收。这通常是由于任务逻辑问题导致线程无法正常结束。

线程泄漏的原因可能是任务逻辑中存在阻塞或死循环,导致线程无法退出,从而无法被线程池回收。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值