Java 高并发核心编程 ----- 线程池原理与实践(上)

文章目录

    • [1. JUC 的线程池架构](#1. JUC 的线程池架构)
    • [2. Executors 的 4 种快捷创建线程池的方法](#2. Executors 的 4 种快捷创建线程池的方法)
    • [3. 线程池的标准创建方法](#3. 线程池的标准创建方法)
    • [4. 向线程池提交任务的 2 种方式](#4. 向线程池提交任务的 2 种方式)
    • [5. 线程池的任务调度流程](#5. 线程池的任务调度流程)
    • [6. 线程工厂(Thread Factory)](#6. 线程工厂(Thread Factory))
    • [7. 任务阻塞队列](#7. 任务阻塞队列)
    • [8. 调度器的钩子方法](#8. 调度器的钩子方法)
    • [9. 线程池的拒绝策略](#9. 线程池的拒绝策略)
    • [10. 线程池的优雅关闭](#10. 线程池的优雅关闭)
    • [11. Executors 快捷创建线程池的潜在问题](#11. Executors 快捷创建线程池的潜在问题)

特此注明 :
Designed By :长安城没有风
Version:1.0
Time:2026.02.01
Location:四川 · 成都

本文为读者阅读《Java 高并发核心编程 卷2》(作者:尼恩)后摘抄部分段落以及整合个人理解后重写书写,推荐感兴趣的朋友可以阅读一下原著,如果有侵权可以私信作者进行删除。

Java 线程的创建非常昂贵,需要 JVM 和 OS 配合完成大量的工作:

  1. 必须要为线程堆栈分配和初始化大量内存块,其中包含至少 1MB 的栈内存。
  2. 需要进行系统调用,以便在 OS 中创建和注册本地线程。

Java 高并发应用频繁创建和销毁线程的操作是非常低效的,而且是不被编程规范所允许的。如何降低 Java 线程的创建成本呢?必须使用到线程池,线程池主要解决了以下两个问题:

  1. 提升性能:线程池能独立负责线程的创建,维护和分配。在执行大量异步任务时,可以不需要自己创建线程,而是将任务交给线程池调度。线程池能尽可能使用空闲的线程执行异步任务,最大限度地对已经创建的线程进行复用,使得性能明显提升。
  2. 线程管理:每个 Java 线程池会保持一些基本的线程统计信息,例如完成的任务数量,空闲时间等,以便对线程有效管理,使得能对所接收到的异步任务进行高效调度。

说明:在主要的大厂编程规范中,不允许在应用中自行显式地创建线程,线程必须通过线程池提供,由于创建和销毁线程上需要时间以及系统资源开销,使用线程池的好处是减少这些开销,解决资源不足的问题。

1. JUC 的线程池架构

在多线程编程中,任务都是一些抽象且离散的工作单元,而线程是使任务异步执行的基本机制。随着应用的扩张,线程和任务管理也变得非常复杂,为了简化这些复杂的线程管理模式,我们需要一个 "管理者" 来统一管理线程及任务分配,这就是线程池。

说明:JUC 就是 java.util.concurrent 工具包的简称,该工具包是从 JDK 1.5 开始加入的,是用于完成高并发,处理多线程的一个工具包。

1. Executor

Executor 是 Java 异步目标任务的 "执行者" 接口,其目标就是来执行目标任务。"执行者" Executor 提供了 execute() 接口来执行已提交的 Runnable 执行目标实例。Executor 作为执行者的角色,其目的是 "任务提交者" 与 "任务执行者" 分离开的机制。它只包含一个函数式方法。

java 复制代码
public interface Executor {
    void execute(Runnable command);
}

2. ExecutorService

ExecutorService 继承自 Executor。它是异步目标任务的 "执行服务者" 接口,对外提供异步任务的接受服务,ExecutorService 提供了 "接受异步任务并转交给执行者" 的方法,如 submit 系列方法,invoke 系列方法等。

java 复制代码
public interface ExecutorService extends Executor {

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

    <T> Future<T> submit(Runnable task, T result);

    Future<?> submit(Runnable task);
 
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException;
  
    <T> T invokeAny(Collection<? extends Callable<T>> tasks)throws InterruptedException, ExecutionException;
  
    <T> T invokeAny(Collection<? extends Callable<T>> tasks,long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}

3. AbstractExecutorService

AbstractExecutorService 是一个抽象类,它实现了 ExecutorService 接口。AbstractExecutorService 存在的目的是为 ExecutorService 中的接口提供默认实现。

4. ThreadPoolExecutor

ThreadPoolExecutor 就是大名鼎鼎的 "线程池" 实现类,它继承于 AbstractExecutorService 抽象类。

ThreadPoolExecutor 是 JUC 线程池的核心实现类。线程的创建和终止需要很大的开销,线程池中预先提供了指定数量的可重用线程,所以使用线程池会节省系统资源,并且每个线程池都维护了一些基础的数据统计,方便线程的管理和监控。

5. ScheduledExecutorService

ScheduledExecutorService 是一个接口,它继承于 ExecutorService。它是一个可以完成 "延时" 和 "周期性" 任务的调度线程池接口,其功能和 Timer/TimerTask 类似。

6. ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor 继承于 ThreadPoolExecutor,它提供了 ScheduledExecutorService 线程池接口中 "延时执行" 和 "周期执行" 等抽象调度方法的具体实现。

ScheduledThreadPoolExecutor 类似于 Timer,但是在高并发程序中,ScheduledThreadPoolExecutor 的性能要优于 Timer。

7. Executors

Executors 是个静态工厂类,它通过静态工厂方法返回 ExecutorService,ScheduledExecutorService 等线程池实例对象,这些静态工厂方法可以理解为一些快捷的创建线程池的方法。

2. Executors 的 4 种快捷创建线程池的方法

方法名 功能简介
newSingleThreadExecutor() 创建只有一个线程的线程池
newFixedThreadPool(int nThreads) 创建固定大小的线程池
newCachedThreadPool() 创建一个不限制线程数量的线程池,任何提交的任务都将立即执行,但是空闲线程会得到及时回收
newScheduledThreadPool() 创建一个可定期或延时执行任务的线程池

1. newSingleThreadExecutor 创建 "单线程化线程池"

该方法用于创建一个 "单线程化线程池",也就是只有一条线程的线程池,所创建的线程池用唯一的工作线程来执行任务,使用此方法创建的线程池能保证所有任务按照指定顺序(FIFO)执行。

java 复制代码
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadDemo {

    static class MyThread implements Runnable{
        private String name;

        @Override
        public void run() {
            Thread.currentThread().setName(name);
            System.out.println(Thread.currentThread().getName() + " run");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }`
        }

        public void setName(String name) {
            this.name = name;
        }
    }
    public static void main(String[] args) {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            MyThread myThread = new MyThread();
            myThread.setName("Thread-" + i);
            executor.execute(myThread);
        }
        executor.shutdown();
    }
}

从上述的输出中可以看出,该线程池有以下特点:

  1. 单线程化的线程池中的任务,是按照提交顺序执行的。
  2. 池中的唯一线程的存活时间是无限的。
  3. 当池中唯一的线程正繁忙时,新提交的任务实例会进入内部的阻塞队列,并且其阻塞队列是无限的。

总体来说,单线程化的线程池所适用的场景是:任务按照提交次序,一个任务接一个任务执行的场景。

以上用例在最后调用 shutdown() 方法用来关闭线程池。执行 shutdown() 方法后,线程池状态变为 SHUTDOWN 状态,此时线程池将拒绝新任务,不能再往线程池中添加新任务,否则会抛出 RejectedExecutionException 异常。此时,线程池不会立刻退出,直到添加到线程池中的任务都已经处理完成才会退出。还有一个与 shutdown() 类似的方法,叫作 shutdownNow() ,执行 shutdownNow() 方法后,线程池状态会立刻变成 STOP,并试图停止所有正在执行的线程,不再处理还在阻塞队列中等待的任务,会返回那些未执行的任务。

2. newFixedThreadPool 创建 "固定数量的线程池"

该方法用于创建一个 "固定线程数量的线程池" ,其唯一的参数用于设置池中线程的 "固定数量" 。

java 复制代码
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadDemo {

    static class Task implements Runnable {
        private String name;

        public Task(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            System.out.println(name + " is running");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(3);

        for (int i = 0; i < 10; i++) {
            Task task = new Task("Task " + i);
            threadPool.execute(task);
        }
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        threadPool.shutdown();
    }
}

上述的代码用例中,创建了一个线程数为 3 的 "固定数量的线程池",然后向其中提交了 10 个任务,从输出结果可以看到,该线程池同时只能执行 3 个任务,剩余的任务会排队等待。

"固定数量的线程池" 的特点大致如下:

  1. 如果线程数没有达到 "固定数量" ,每次提交一个任务池中就创建一个新线程,知道线程达到线程池固定的数量。
  2. 线程池的大小一旦达到 "固定数量" 就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
  3. 在接受异步任务的执行目标实例时,如果池中的所有线程均在繁忙状态,新任务会进入阻塞队列中(一个无限的阻塞队列)。

"固定数量的线程池" 的适用场景:需要任务长期执行的场景。"固定数量的线程池" 的线程数能够比较稳定的保证一个数,避免频繁回收线程和创建线程,故适用于处理 CPU 密集型的任务,在 CPU 被工作线程长时间使用的情况下,能确保尽可能地少分配线程。

"固定数量的线程池" 的弊端:内部使用无界队列来存放任务,当大量任务超过线程池最大容量需要处理时,队列无限增大,使服务器资源迅速耗尽。

3.newCachedThreadPool 创建 "可缓存线程池"

该方法用于创建一个 "可缓存线程池" ,如果线程池内的某些线程无事可干成为空闲线程,"可缓存线程池" 可灵活回收这些空闲线程。

"可缓存线程池" 的特点大致如下:

  1. 在接受新的异步任务 target 执行目标实例时,如果池内所有线程繁忙,此线程池就会添加新的线程来处理任务。
  2. 此线程池不会对线程池大小进行限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
  3. 如果部分线程空闲,也就是存量线程的数量超过了处理任务数量,就会回收线程(60 秒不执行任务)。

"可缓存线程池" 的适用场景:需要快速处理突发性强,耗时较短的任务场景,如 Netty 的 NIO 处理场景,REST API 接口的瞬时削峰场景。"可缓存线程池" 的线程数量不固定,只要有空闲线程就会被回收;接收到的新异步任务执行目标,查看是否有线程处于空闲状态,如果没有就直接创建新的线程。

"可缓存线程池" 的弊端:线程池没有最大线程数量限制,如果大量的异步任务执行目标实例同时提交,可能会因线程过多而导致资源浪费。

4.newScheduledThreadPool 创建 "可调度线程池"

该方法用于创建一个 "可调度线程池",即一个提供 "延时" 和 "周期性" 任务的调度功能的 ScheduledExecutorService 类型的线程池。Executors 提供了多个创建 "可调度线程池" 工厂方法。

java 复制代码
//方法一:创建一个可调度线程池,池内仅含有一个线程
public static ScheduledExecutorService newSingleThreadScheduledExecutor();
//方法二:创建一个可调度线程池,池内含有N个线程,N的值为输入参数corePoolSize
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize);

newSingleThreadScheduledExecutor() 工厂方法所创建的仅含有一个线程的可调度线程池,适用于调度串行化任务,也就是一个任务接一个任务地串行化调度执行。

使用 newScheduledThreadPool(int corePoolSize) 快捷工厂方法创建一个"可调度线程池"的测试用例,其代码如下:

java 复制代码
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ThreadDemo {

    static class Task implements Runnable {
        private String name;

        public Task(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            System.out.println(name + " is running");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(2);

        for (int i = 0; i < 10; i++) {
            Task task = new Task("Task " + i);
            threadPool.scheduleAtFixedRate(task, 4, 4, TimeUnit.SECONDS);
        }
        Thread.sleep(2000);
        threadPool.shutdown();

    }
}

newScheduledThreadPool() 工厂方法可以创建一个执行 "延时" 和 "周期性" 任务可调度线程池,所创建的线程池为 ScheduledExecutorService 类型的实例。ScheduledExecutorService 接口有多个重要的接受被调目标方法,其中 scheduledAtFixedRate 和 scheduledFixedDelay 使用的比较多。

ScheduleExecutorService接收被调目标任务方法之一 scheduleAtFixedRate 方法的定义如下:

java 复制代码
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
// Runnable command 异步任务 target 执行目标实例
// long initialDelay 首次执行延时
// long period 两次执行最小时间间隔
// TimeUnit unit 所设置的时间的计时单位,如 TimeUnit.SECONDS 常量

ScheduleExecutorService接收被调目标任务方法之二scheduleWithFixedDelay方法的定义如下:

java 复制代码
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)
// Runnable command 异步任务 target 执行目标实例
// long initialDelay 首次执行延时
// long  delay 前一次执行结束到下一次执行开始的间隔时间(间隔执行延时时间)
// TimeUnit unit 所设置的时间的计时单位,如 TimeUnit.SECONDS 常量

当被调任务的执行时间大于指定的时间间隔时,ScheduledExecutorService 并不会在创建一个新的线程去并发执行这个任务,而是等待前一次调度执行完毕。

以上是通过 JUC 的 Executors 中 4个主要的快捷创建线程池方法。为何 JUC 要提供工厂方法呢?原因是使用 ThreadPoolExecutor ,ScheduledThreadPoolExecutor 构造器去创建普通线程池,可调度线程池比较复杂,这些构造器会设计大量的复杂参数。尽管 Executors 的工厂方法使用方便,但是在生产场景下被很多企业的开发规范所禁用。

3. 线程池的标准创建方法

大部分企业的开发规范都会禁止使用快捷线程池(具体原因稍后介绍),要求通过标准构造器 ThreadPoolExecutor 去构造工作线程池。Executors 工厂类中创建线程池的快捷工厂方法实际上是调用 ThreadPoolExecutor 线程池的构造方法完成的。ThreadPoolExecutors 构造方法有多个重载版本,其中一个比较重要的构造器如下:

java 复制代码
public ThreadPoolExecutor(

		   int corePoolSize,                  // 核心线程数,即使线程空闲,也不会被回收
           int maximumPoolSize,               // 线程数的上限
           long keepAliveTime,                // 线程最大空闲时间
           TimeUnit unit,                     // 时间单位
           BlockingQueue<Runnable> workQueue, // 任务的排队队列
           ThreadFactory threadFactory,       // 新线程的产生方式
           RejectedExecutionHandler handler)  // 拒绝策略

接下来对这些参数做一下具体介绍。

1. 核心和最大线程数量

参数 corePoolSize 用于设置核心(Core)线程池数量,参数 maximumPoolSize 用于设置最大线程数量。线程池执行器将会根据 corePoolSize 和 maximumPoolSize 自动地维护线程池中的工作线程,大致的规则为:

  1. 当在线程池接收到的新任务,并且当前工作线程数少于 corePoolSize 时,即使其他工作线程处于空闲状态,也会创建一个新线程来处理该请求,直到线程数达到 corePoolSize。
  2. 如果当前工作线程数多于 corePoolSize 数量,小于 maximumPoolSize 数量,那么仅当任务排队队列已满时才会创建新线程。通过设置 corePoolSize 和 maximumPoolSize 相同,可以创建一个固定大小的线程池。
  3. 当 maximumPoolSize 被设置为无界值时(如 Integer.MAX_VALUE),线程池可以接受任意数量的并发任务。
  4. corePoolSize 和 maximumPoolSize 不仅能在线程池构造时设置,也可以调用 setCorePoolSize() 和 setMaximumPoolSize() 两个方法进行动态更改。

2. BlockingQueue

BlockingQueue(阻塞队列)的实例用于暂时接受到的异步任务,如果线程池的核心线程都在忙,那么所接收到的目标任务缓存在阻塞队列中。

3. keepAliveTime

线程构造器的 keepAliveTime (空闲线程存活时间)参数用于设置池内线程最大空闲时间或者说保活时长,如果超过这个时间,默认情况下空闲的非核心线程都会被回收。

如果线程池在使用过程中提交任务的频率变高,也可以调用 setKeepAliveTime(long time, TimeUnit unit) 进行线程存活时间的动态调整,也可以将时长延长。如果需要防止空闲线程被终止,可以将时间设置为无限大。

默认情况下,空闲超时策略仅适用于存在超过 corePoolSize 线程的情况。但是如果调用了 allowCoreThreadTimeOut() 方法,并且传入参数 true,则空闲超时策略也适用于核心线程。

4. 向线程池提交任务的 2 种方式

向线程池提交任务的两种方式,大致如下:

方法一:调用 execute() 方法,例如:

java 复制代码
public void execute(Runnable command);

方法二:调用 submit() 方法,例如:

java 复制代码
<T> Future<T> submit(Callable<T> task);

<T> Future<T> submit(Runnable task, T result);

Future<?> submit(Runnable task);

以上的 submit 和 execute 两类方法区别在哪里呢?大致有三点:

  1. 二者所接受的参数不一样

execute() 方法只能接收 Runnable 类型的参数,而 submit() 方法可以接收 Callable、Runnable 两种类型的参数。Callable 类型的任务是可以返回执行结果的,而 Runnable 类型的任务不可以返回执行结果。Callable 是 JDK 1.5 加入的执行目标接口,作为 Runnable 的一种补充,允许有返回值,允许抛出异常。Runnable 和 Callable 的主要区别为:Callable 允许有返回值,Runnable 不允许有返回值;Runnable 不允许抛出异常,Callable 允许抛出异常。

  1. submit() 提交任务后会有返回值,而 execute() 没有

execute() 方法主要用于启动任务的执行,而并不关心任务的执行结果和可能发生的异常,submit() 方法也用于启动任务的执行,但是启动之后会返回 Future 对象,代表一个异步执行实例,可以通过该异步执行实例去获取结果。

  1. submit() 方便 Exception 处理

execute() 方法在启动任务的执行后,并不关心任务执行过程中可能发生的异常。而 submit() 方法返回 Future 对象,可以进行异步执行过程中的异常捕获。

通过 submit() 返回 Future 对象获取结果以及捕捉异常

java 复制代码
import java.util.Random;
import java.util.concurrent.*;

public class ThreadDemo {

    static  class Task implements Callable<Integer> {
        Random random = new Random();
        @Override
        public Integer call(){
            return random.nextInt();
        }
    }

    public static void main(String[] args){

        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
                2,
                4,
                60,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(2)
        );

        Future<Integer> submit = poolExecutor.submit(new Task());
        Integer result = null;
        try {
            result = submit.get();
        } catch (InterruptedException | ExecutionException e) {
            throw new RuntimeException(e);
        }
        
        System.out.println(result);
        poolExecutor.shutdown();
    }
}

5. 线程池的任务调度流程

线程池的任务调度流程(包含接受新任务和执行下一个任务)大致如下:

  1. 如果当前工作线程数小于核心线程池数量,执行器总是优先创建一个任务线程,而不是线程队列中获取一个空闲线程。

  2. 如果线程池中总的任务数量大于核心线程池数量,新接受的任务将被加入到阻塞队列中,一直到阻塞队列已满。在核心线程池数量已经用完,阻塞队列没有满的情况下,线程池不会为新任务创建一个新线程。

  3. 当完成一个任务的执行时,执行器总是优先从阻塞队列中获取下一个任务,并开始执行一直到阻塞队列为空,其中所有的缓存任务被取光。

  4. 在核心线程池数量已经用完,阻塞队列也已经满了的场景下,如果线程池接收到新的任务,将会为新任务创建一个线程(非核心线程),并且立即开始执行任务。

  5. 在核心线程都用完,阻塞队列已满的情况下,一直会创建新线程去执行新任务,直到池内的线程总数超出 maximumPoolSize 。如果线程池的线程总数超过 maximumPoolSize ,线程池就会拒绝接受任务,当新任务过来时,会为新任务执行拒绝策略。

总体的线程池的任务调度流程大致如图:

给大家两条小 tips :

  1. 核心和最大线程数量,BlockingQueue 队列等参数如果配置的不合理,可能会造成异步任务得不到预期的并发执行,造成严重的排队等待现象。
  2. 线程池的调度器创建线程的一条重要的规则是:在corePoolSize 已满之后,阻塞队列也要满,才会创建新线程。

下面是一个有关线程池调度的面试真题:

一个线程池的核心线程数为 10,最大线程数为 20,阻塞队列的容量为 30,现在提交 45 个任务,每个任务耗时 500 毫秒,完成这批任务总共需要多长时间?

6. 线程工厂(Thread Factory)

ThreadFactory 是 Java 线程工厂接口,这是一个非常简单的接口,具体如下:

java 复制代码
public interface ThreadFactory {
    Thread newThread(Runnable r);
}

在调用 ThreadFactory 唯一的方法 newThread() 创建新线程时,可以更改创建新线程的名字,线程组,优先级等。如果 newThread() 方法返回值为 null,表示线程工厂未能成功创建线程,线程池可能无法执行任务。

使用 Executors 创建新的线程池时,也可以基于 ThreadFactory 创建,在创建新线程池时可以指定使用 ThreadFactory 实例,默认使用 Executors.defaultThreadFactory() 。

7. 任务阻塞队列

Java 中的阻塞队列(BlockingQueue)与普通队列相比有一个重要的特点:在阻塞队列为空时,会阻塞当前线程获取元素的操作。具体来说,在一个线程从一个空的阻塞队列中获取元素时线程会被阻塞,直到阻塞队列有了新的元素;当队列中有元素后,被阻塞的线程会自动被唤醒(唤醒过程不需要用户干预)。

Java 线程池使用 BlockingQueue 存放接受到的异步任务,BlockingQueue 是 JUC 包的一个超级接口,比较常见的实现类有:

  1. ArrayBlockingQueue:是一个数组实现的有界阻塞队列(有界队列),队列中的元素按照 FIFO 排序。ArrayBlockingQueue 在创建时必须设置大小,线程池接受的任务超出 corePoolSize 数量时,任务被缓存到该阻塞队列中,任务缓存的数量只能为创建时的大小,若该阻塞队列满,则会为新的任务创建新线程(非核心线程),知道线程池中的线程数达到 maximumPoolSize。
  2. LinkedBlockingQueue:是一个基于链表实现的阻塞队列,按 FIFO 排序任务,可以设置容量(有界队列),不设置容量则默认使用 Integer.MAX_VALUE 作为容量(无界队列)。该队列的吞吐量高于 ArrayBlockingQueue。
  3. PriorityBlockingQueue:是具有优先级的无界队列。
  4. DelayQueue:这是一个无界阻塞延迟队列,底层基于 PriorityBlockingQueue 实现,队列中每个元素都有过期时间。当从队列获取元素时(元素出队),只有已经过期的元素才会出队,而队列头部的元素是最先过期的元素。快捷工厂方法 Executors.newScheduledThreadPool 所创建的线程池使用此队列。
  5. SynchronousQueue:同步队列,是一个不存储元素的队列,每个插入操作必须等到另一个线程的调用移除操作,否则插入一直处于阻塞状态,其吞吐量通常高于 LinkedBlockingQueue,快捷工厂方法 Executors.newCachedThreadPool 所创建的线程池使用此队列。与前面的队列相比,这个队列比较特殊,它不会保存提交的任务,而是直接新建一个线程来执行新来的任务。

8. 调度器的钩子方法

ThreadPoolExecutor 线程池调度器为每个任务执行前后都提供了钩子方法。ThreadPoolExecutor 类提供了三个钩子方法(空方法),这三个空方法一般用作被子类重写,具体如下:

java 复制代码
//任务执行之前的钩子方法(前钩子)
protected void beforeExecute(Thread t, Runnable r) { }

//任务执行之后的钩子方法(后钩子)
protected void afterExecute(Runnable r, Throwable t) { }

//线程池停止时的钩子方法(停止钩子)
protected void terminated() { }

//为什么设计成 protected?因为它不是业务 API,不想让你随便调用,但允许你扩展行为。
  1. beforeExecute 任务执行之前的钩子方法

线程池工作线程在异步执行目标实例前调用此钩子方法。它是 ThreadPoolExecutor 在内部调度任务时自动使用的。它出现的地方不在业务代码里,而在线程池的执行链路中。我们可以把它想象成:线程池在干活前,先低声说一句"我要开始了"。我们用它的方式只有一种:继承 ThreadPoolExecutor 并重写它。

此方法可以用来记录任务开始时间(配合 afterExecute 算耗时),任务执行前的统一校验,打日志、打 traceId(链路追踪)等等。

  1. afterExecute 任务执行之后的钩子方法

线程池工作线程在异步执行目标实例后调用此钩子方法。它仍然是 ThreadPoolExecutor 在内部调度任务时自动使用的。默认不实现任何方法,可以继承ThreadPoolExecutor 并重写它。

afterExecute 典型用途包括:统计任务执行耗时(配合 beforeExecute),统一异常日志、报警,释放 ThreadLocal / MDC,采集指标(成功数、失败数),做任务级别的 tracing 结束标记。

  1. terminated 线程池停止时的钩子方法

terminated 钩子方法在 Executor 终止时调用,默认实现不执行任何操作。

说明:beforeExecute 和 afterExecute 两个方法在每个任务执行前后被调用,如果钩子方法引发异常,内部工作线程可能失败并突然停止。

9. 线程池的拒绝策略

在线程池的任务缓存队列为有界队列的时候,如果队列满了,并且线程数已经达到了 maximumPoolSize,提交任务到线程池会被拒绝。总体来说,任务被拒绝有两种情况:

  1. 线程池关闭
  2. 工作队列已满并且 maximumPoolSize 已满

无论以上哪种情况任务被拒绝,线程池都会调用 RejectExecutionHandler 实例的 rejectExecution() 方法,RejectExecutionHandler 是拒绝策略的接口,JUC 为该接口提供了以下几种实现:

  • AbortPolicy:拒绝策略
  • DiscardPolicy:抛弃策略
  • DiscardOldestPolicy:抛弃最老任务策略
  • CallerRunsPolicy:调用者执行策略
  • 自定义策略
  1. AbortPolicy

使用该策略时,如果线程池已经满了,新任务就会被拒绝,并且抛出 RejectExecutionException 异常。该策略是线程池的默认实现策略。

  1. DiscardPolicy

该策略是 AbortPolicy 的 Silent(安静)版本,如果线程池队列满了,新任务就会直接被丢掉,并且不会有任何异常抛出。

  1. DiscardOldestPolicy

抛弃最老任务策略,也就是说如果队列满了,就会将最早进入队列的任务抛弃掉,从队列中腾出空间,在尝试加入队列。因为队列是头进尾出,队头元素是最老的,所以每次都是移除队头元素后在尝试入队。

  1. CallerRunsPolicy

调用者执行策略。在新任务被添加到线程池时,如果添加失败,那么提交任务线程会自己去执行任务,不会使用使用线程池中的线程去执行任务。

  1. 自定义策略

如果以上拒绝策略都不符合需求,那么可以自定义一个拒绝策略,实现 RejectExceptionHandler 接口的 rejectExecution() 方法即可。

10. 线程池的优雅关闭

一般情况下,线程池启动后建议手动关闭。在介绍线程池的优雅关闭之前,我们先了解一下线程池状态。线程池总共存在5种状态,定义在 ThreadPoolExecutor 类中,具体代码如下:

java 复制代码
public class ThreadPoolExecutor extends AbstractExecutorService {
    // runState is stored in the high-order bits
    private static final int RUNNING    = -1 << COUNT_BITS;
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    private static final int STOP       =  1 << COUNT_BITS;
    private static final int TIDYING    =  2 << COUNT_BITS;
    private static final int TERMINATED =  3 << COUNT_BITS;
}

线程池的5种状态具体如下:

  1. RUNNING:线程池创建之后的初始状态,这种状态下可以执行任务。
  2. SHUTDOWN:该状态下线程池不再接受新任务,但是会将工作队列中的任务执行完毕.
  3. STOP:该状态下线程池不再接受新任务,也不会处理工作队列中的剩余任务,并且将会中断所有工作线程。
  4. TIDYING:该状态下所有任务都已终止或者处理完成,将会执行 terminated() 钩子方法。
  5. TERMINATED:执行完 terminated() 钩子方法之后的状态。

线程池的状态转换规则为:

  1. 线程池创建之后状态为RUNNING。
  2. 执行线程池的 shutdown() 实例方法,会使线程池状态从 RUNNING 转变为 SHUTDOWN。
  3. 执行线程池的 shutdownNow() 实例方法,会使线程池状态从 RUNNING 转变为 STOP。
  4. 当线程池处于 SHUTDOWN 状态,执行其 shutdownNow() 方法会将其状态转变为 STOP。
  5. 等待线程池的所有工作线程停止,工作队列清空之后,线程池状态会从 STOP 转变为 TIDYING。
  6. 执行完terminated()钩子方法之后,线程池状态从TIDYING转变为TERMINATED。

优雅地关闭线程池主要涉及的方法有3种:

  1. shutdown:是 JUC 提供一个有序关闭线程池的方法,此方法会等待当前工作队列中的剩余任务全部执行完成之后才会执行关闭,但是此方法被调用之后线程池的状态转变为 SHUTDOWN,线程池不会再接收新的任务。
  2. shutdownNow:是 JUC 提供一个立即关闭线程池的方法,此方法会打断正在执行的工作线程,并且会清空当前工作队列中的剩余任务,返回的是尚未执行的任务。
  3. awaitTermination:等待线程池完成关闭。在调用线程池的 shutdown() 与 shutdownNow() 方法时,当前线程会立即返回,不会一直等待直到线程池完成关闭。如果需要等到线程池关闭完成,可以调用 awaitTermination() 方法。

11. Executors 快捷创建线程池的潜在问题

在很多公司(如阿里,华为等)的编程规范中,非常明确的禁止使用 Executors 快捷创建线程池,为什么呢?这里从源码讲起,介绍使用 Executors 工厂方法快捷创建线程池将会面临的潜在问题。

1. 使用 Executors 创建 "固定数量的线程池" 的潜在问题

java 复制代码
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
            0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>());
}

newFixedThreadPool 工厂方法返回一个 ThreadPoolExecutor 实例,该线程池实例的 corePoolSize 数量为参数 nThread , 其 maximumPoolSize 数量也为参数 nThread , 其 workQueue 属性的值为 LinkedBlockingQueue() 无界阻塞队列。

使用 Executors 创建 "固定数量的线程池" 的潜在问题主要就存在于其 workQueue 上,其值为 LinkedBlockingQueue(无界阻塞队列)。如果任务提交速度大于任务处理速度,就会造成队列中大量的任务等待。如果队列很大,很有可能会导致 JVM 出现 OOM 异常,即内存资源耗尽。

2. 使用 Executors 创建 "单线程化线程池" 的潜在问题

java 复制代码
public static ExecutorService newSingleThreadExecutor() {
    return new Executors.FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                    0L, TimeUnit.MILLISECONDS,
                    new LinkedBlockingQueue<Runnable>()));
}

以上代码首先通过调用工厂方法 newFixedThreadPool(1) 创建一个数量为1的"固定大小线程池",然后使用FinalizableDelegatedExecutorService 对该"固定大小线程池"进行包装,这一层包装的作用是防止线程池的 corePoolSize 被动态地修改。

使用 Executors 创建的"单线程化线程池"与"固定大小线程池"一样,其潜在问题仍然存在与其 workQueue 属性上,该属性的值为 LinkedBlockingQueue(无界阻塞队列)。如果任务提交速度持续大于任务处理速度,就会造成队列大量阻塞。如果队列很大,很有可能导致 JVM 的 OOM 异常,甚至造成内存资源耗尽。

3. 使用 Executors 创建 "可缓存线程池" 的潜在问题

java 复制代码
public static ExecutorService newCachedThreadPool(){
		return new ThreadPoolExecutor(
				0, //核心线程数
				Integer.MAX_VALUE, //最大线程数
				60L, //线程最大空闲(Idle)时长
				TimeUnit.MILLISECONDS, //时间单位:毫秒
				new SynchronousQueue<Runnable>()); //任务的排队队列,无界队列
} 

以上代码通过调用 ThreadPoolExecutor 标准构造器创建一个核心线程数为 0、最大线程数不设限制的线程池。所以,理论上"可缓存线程池"可以拥有无数个工作线程,即线程数量几乎无限制。"可缓存线程池" 的 workQueue 为SynchronousQueue 同步队列,这个队列类似于一个接力棒,入队与出队必须同时传递,正因为 "可缓存线程池" 可以无限制创建线程,不会有任务等待,所以才使用 SynchronousQueue。

当 "可缓存线程池" 有新任务到来时,新任务会被插入到 SynchronousQueue 实例中,由于 SynchronousQueue 是同步队列,因此会在池中寻找可用线程来执行,若有可用线程则执行,若没有可用线程,则线程池会创建一个线程来执行该任务。

SynchronousQueue 是一个比较特殊的阻塞队列实现类,SynchronousQueue 没有容量,每一个插入操作都要等待对应的删除操作,反之每个删除操作都要等待对应的插入操作。也就是说,如果使用 SynchronousQueue,提交的任务不会被真实地保存,而是将新任务交给空闲线程执行,如果没有空闲线程,就创建线程,如果线程数都已经大于最大线程数,就执行拒绝策略。使用这种队列需要将 maximumPoolSize 设置得非常大,从而使得新任务不会被拒绝。

使用 Executors 创建的 "可缓存线程池" 的潜在问题存在于其最大线程数量不设上限。由于其 maximumPoolSize 的值为 Integer.MAX_VALUE(非常大),可以认为是无限创建线程的,如果任务提交较多,就会造成大量的线程被启动,很有可能造成 OOM 异常,甚至导致 CPU 线程资源耗尽。

4. 使用 Executors 创建 "可调度线程池" 的潜在问题

java 复制代码
    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }

    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE,
              DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
              new DelayedWorkQueue());
    }

以上代码创建一个 ThreadPoolExecutor 实例,其 corePoolSize 为传递来的参数,maximumPoolSize 为 Integer.MAX_VALUE,表示线程数不设上限,其 workQueue 为一个 DelayedWorkQueue 实例,这是一个按到期时间升序排序的阻塞队列。

使用 Executors 创建的"可调度线程池"的潜在问题存在于其最大线程数量不设上限。由于其线程数量不设限制,如果到期任务太多,就会导致 CPU 的线程资源耗尽。实际上,通过源码分析可以看出,"可调度线程池"的潜在问题首先还是无界工作队列(任务排队的队列)长度都为 Integer.MAX_VALUE,可能会堆积大量的任务,从而导致OOM 甚至耗尽内存资源的问题。

5. 总结

以上内容分别梳理了 Executors 四个工厂方法所创建的线程池将面临的潜在问题。总结起来,使用 Executors 去创建线程池主要的弊端如下:

(1)FixedThreadPool和SingleThreadPool

这两个工厂方法所创建的线程池,工作队列(任务排队的队列)长度都为 Integer.MAX_VALUE,可能会堆积大量的任务,从而导致 OOM(即耗尽内存资源)。

(2)CachedThreadPool和ScheduledThreadPool

这两个工厂方法所创建的线程池允许创建的线程数量为 Integer.MAX_VALUE,可能会导致创建大量的线程,从而导致 OOM 问题。

网上众人和阿里编程规范,没有深入研读源码,被 ScheduledThreadPool 的最大线程数没有限制的参数所误导。通过源码分析发现,最大线程数参数 maximumPoolSize 对可调度线程池并未起作用,实际上,ScheduledThreadPool 内部的线程数最多为核心线程数,关键的问题还是在于其工作队列上。该线程池的工作队列(任务排队的队列)长度都为 Integer.MAX_VALUE,可能会堆积大量的任务,从而导致 OOM 问题。

虽然 Executors 工厂类提供了构造线程池的便捷方法,但是对于服务器程序而言,大家应该杜绝使用这些便捷方法,而是直接使用线程池ThreadPoolExecutor的构造器,从而有效避免由于使用无界队列可能导致的内存资源耗尽,或者由于对线程个数不做限制而导致的CPU资源耗尽等问题。

所以,大厂的编程规范都不允许使用 Executors 创建线程池,而是要求使用标准构造器ThreadPoolExecutor 创建线程池。

相关推荐
Remember_9933 小时前
Spring 核心原理深度解析:Bean 作用域、生命周期与 Spring Boot 自动配置
java·前端·spring boot·后端·spring·面试
风流倜傥唐伯虎4 小时前
java多线程打印
java·多线程
80530单词突击赢4 小时前
云原生时代:.NET与Java的K8s进化论
java
hhy_smile4 小时前
Special method in class
java·开发语言
我命由我123454 小时前
Android 开发 Room 数据库升级问题:A migration from 6 to 7 was required but not found.
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
黄筱筱筱筱筱筱筱4 小时前
7.适合新手小白学习Python的异常处理(Exception)
java·前端·数据库·python
Stecurry_304 小时前
Springboot整合SpringMVC --从0到1
java·spring boot·后端
Serene_Dream4 小时前
NIO 的底层机理
java·jvm·nio·mmap
skywalker_114 小时前
多线程&JUC
java·开发语言·jvm·线程池