两万字长文-最全的线程池详解

两万字长文-最全的线程池详细介绍,是一篇全面深入的Java线程池探讨,详解Executors工具与ThreadPoolExecutor的常用方法,示范实战案例,并剖析ThreadPoolExecutor的各种参数与最佳实践。从基础到高级,为读者提供了丰富的线程池知识,既是API工具,也是面试宝典。无论您是初学者还是专业开发者,都将在这篇长文中找到深度解析与实用技巧,助您在多线程编程领域游刃有余。

一、 无界线程池 newCachedThreadPool

Java中Executor框架提供的一个工厂方法,用于创建一个根据需要创建新线程的线程池 。这种线程池在执行任务之前会尝试重用现有的空闲线程,如果没有可用的线程,则会创建新的线程。如果线程在60秒内没有被使用,它将被终止并从缓存中移除

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

public class CachedThreadPoolDemo {

    public static void main(String[] args) {
        // 创建一个根据需要创建新线程的缓存线程池
        ExecutorService executorService = Executors.newCachedThreadPool();

        // 提交一些任务给线程池
        for (int i = 1; i <= 5; i++) {
            final int taskId = i;
            executorService.submit(() -> {
                System.out.println("Task " + taskId + " is executing by " + Thread.currentThread().getName());
                try {
                    Thread.sleep(2000); // 模拟任务执行
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        // 关闭线程池
        executorService.shutdown();
    }
}

创建了一个根据需要创建新线程的缓存线程池。然后,我们提交了5个任务给线程池。由于线程池的特性是根据需要动态创建线程,所以这里可能会创建新的线程来执行任务。如果线程在60秒内没有被使用,它将被终止并从缓存中移除。

缓存线程池适用于处理大量的短生命周期任务,因为它可以根据需要动态地调整线程池的大小,避免了创建过多线程导致资源浪费的问题。

1.1 无界线程池的创建过程是可定制的

如果你想要自定义线程的创建方式 ,你可以使用 newCachedThreadPool 方法的重载版本,该版本接受一个 ThreadFactory 参数,允许你提供自定义的线程工厂

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

public class CachedThreadPoolWithCustomThreadFactory {

    public static void main(String[] args) {
        // 创建自定义线程工厂
        ThreadFactory threadFactory = new ThreadFactory() {
            @Override
            public Thread newThread(Runnable runnable) {
                Thread thread = new Thread(runnable);
                thread.setName("CustomThread-" + thread.getId());
                thread.setPriority(Thread.NORM_PRIORITY);
                thread.setDaemon(false);
                return thread;
            }
        };

        // 创建一个根据需要创建新线程的缓存线程池,使用自定义线程工厂
        var executorService = Executors.newCachedThreadPool(threadFactory);

        // 提交一些任务给线程池
        for (int i = 1; i <= 5; i++) {
            final int taskId = i;
            executorService.submit(() -> {
                System.out.println("Task " + taskId + " is executing by " + Thread.currentThread().getName());
                try {
                    Thread.sleep(2000); // 模拟任务执行
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        // 关闭线程池
        executorService.shutdown();
    }
}

通过提供自定义的 ThreadFactory 来实现线程的定制。在 ThreadFactory 的实现中,我们设置了线程的名称、优先级和是否为守护线程。然后,我们使用自定义的线程工厂创建了一个根据需要创建新线程的缓存线程池。这样就能够按照自己的需求创建线程

1.2 无界线程池的优缺点

newCachedThreadPool 创建的线程池是一个无界线程池,它具有以下特点:

1.2.1 优点:

  1. 动态调整线程数量: 无界线程池会根据需求动态地创建新线程,无上限地适应任务的数量。当有新任务提交时,如果池中没有空闲线程,则会创建一个新线程来处理任务。这使得线程池能够灵活地适应工作负载的变化。

  2. 任务处理速度快: 由于可以根据需要创建新线程,无界线程池在瞬时负载较高的情况下能够更快地响应任务。

1.2.2 缺点:

  1. 可能导致资源耗尽: 由于线程数量没有上限,当有大量任务提交时,可能会创建大量线程,导致系统资源(如内存)耗尽。

  2. 可能导致过度竞争: 在高并发情况下,大量线程的创建可能导致线程之间的竞争,从而影响性能。

  3. 可能导致任务堆积: 如果任务的执行时间较长,而新任务不断提交,可能导致线程池中积累大量未完成的任务,影响系统的稳定性。

  4. 不适用于长期运行的任务: 对于长期运行的任务,无界线程池可能会导致创建大量的线程,而这些线程在任务完成后不会被回收,最终可能耗尽系统资源。

无界线程池适用于任务短暂、处理速度快的场景,但在长时间运行的任务或者负载较高的情况下,可能需要考虑其他类型的线程池,例如有界线程池或者使用任务队列进行任务排队。在选择线程池类型时,需要根据具体应用场景和系统资源来进行权衡。

二、有界线程池 newFixedThreadPool

Java中Executor框架提供的一个工厂方法**,用于创建固定大小的线程池**。这种线程池在应用程序的整个生命周期内都保持相同数量的线程 ,当有新的任务提交时,如果线程池中的线程数未达到最大值,将会创建新的线程来处理任务,否则任务将会被放入队列等待执行

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

public class FixedThreadPoolDemo {

    public static void main(String[] args) {
        // 创建一个固定大小为3的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(3);

        // 提交一些任务给线程池
        for (int i = 1; i <= 5; i++) {
            final int taskId = i;
            executorService.submit(() -> {
                System.out.println("Task " + taskId + " is executing by " + Thread.currentThread().getName());
                try {
                    Thread.sleep(2000); // 模拟任务执行
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        // 关闭线程池
        executorService.shutdown();
    }
}

创建了一个固定大小为3的线程池。然后,我们提交了5个任务给线程池。由于线程池的大小是3,因此最多只能同时执行3个任务,而其余的任务将会被放入队列等待执行。线程池会在有空闲线程时从队列中取出任务执行。

2.1 优缺点

2.1.1 优点:

  1. 控制资源使用: 有界线程池限制了线程的数量,防止线程数量无限增长,从而有效控制了系统资源的使用。
  2. 避免资源耗尽: 由于线程数量是有限的,不会无限制地创建新线程。这有助于避免系统资源(如内存)被大量线程耗尽。
  3. 稳定性: 有界线程池可以更好地保持系统的稳定性,避免过度竞争和任务堆积。

2.1.2 缺点:

  1. 灵活性差: 有界线程池的线程数量是固定的,不能动态调整。如果负载较大,可能导致线程不足;如果负载较小,可能会浪费资源。
  2. 不适用于瞬时高并发: 在某些瞬时高并发的场景下,有界线程池可能无法及时响应大量的任务,导致一些任务需要等待执行。
  3. 可能导致线程饥饿: 如果设置的线程数较小,并且任务提交速度较快,可能导致部分任务一直等待执行,产生线程饥饿的情况。

有界线程池适用于**相对稳定的工作负载,能够限制线程数量,防止资源耗尽。**在选择线程池类型时,需要根据应用场景和性能需求来进行权衡。

三、 newSingleThreadExecutor

用于创建一个包含单个线程的线程池。这个线程池确保所有提交的任务按照顺序执行,即每次只有一个线程在执行任务。如果这个线程因为异常而结束,会有另一个线程取代它,保持线程池中始终存在一个活动线程。

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

public class SingleThreadExecutorDemo {

    public static void main(String[] args) {
        // 创建一个包含单个线程的线程池
        ExecutorService executorService = Executors.newSingleThreadExecutor();

        // 提交一些任务给线程池
        for (int i = 1; i <= 5; i++) {
            final int taskId = i;
            executorService.submit(() -> {
                System.out.println("Task " + taskId + " is executing by " + Thread.currentThread().getName());
                try {
                    Thread.sleep(2000); // 模拟任务执行
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        // 关闭线程池
        executorService.shutdown();
    }
}

创建了一个包含单个线程的线程池,然后提交了5个任务给线程池。由于是单线程池,所以每次只有一个线程在执行任务,保证了任务的顺序执行。这种线程池适用于需要按顺序执行任务的场景,且在任务执行期间不需要并发执行。

3.1 优势

为什么只说优势?

这种单一线程的线程池使用场景是非常明确的,这里只是想说明为什么存在单线程的线程池 是不是有种干嘛不直接使用线程的疑惑

使用 newSingleThreadExecutor 创建单线程池的主要优势在于对任务的顺序执行以及异常处理的管理。以下是一些使用单线程池的优势:

  1. 顺序执行: 单线程池保证任务按照提交的顺序执行。这对于需要按照特定顺序处理任务的场景非常有用,确保任务之间的顺序关系得到维护。
  2. 线程复用: 单线程池中只有一个线程,该线程会被重复使用来执行不同的任务。这减少了线程的创建和销毁开销,提高了线程的复用性。
  3. 异常管理: 如果任务抛出异常而导致线程终止,单线程池会创建一个新的线程来取代原来的线程,确保线程池中总是有一个可用的线程,防止因为异常而导致整个应用程序崩溃。
  4. 方便的线程控制: 单线程池通过一个线程来处理任务,使得在某些场景下更容易进行线程控制和调试。例如,你可以更容易地追踪任务的执行,查看任务的日志,进行线程调试等。
  5. 任务队列的管理: 单线程池内部维护了一个任务队列,将任务按照提交的顺序进行排队。这有助于管理任务的执行顺序和控制任务的并发度。

虽然在某些简单的场景下直接使用一个线程可能足够,但单线程池的引入可以提供更多的控制和管理,使得在复杂的应用中更容易维护和调试。同时,线程池的使用也符合并发编程的最佳实践,能够有效地管理线程的生命周期,防止资源泄漏和浪费。

四、ThreadPoolExecutor

查看源码后会发现,上述工厂模式中的封装方法都是对ThreadPoolExecutor 进行封装。有点外观模式的味道。

Executors 工具类提供了一些方便的方法,适用于许多常见的情况,而 ThreadPoolExecutor 则提供了更高度定制化的选项,适用于需要更精细控制的场景。在选择使用哪个方法时,可以根据具体的需求和场景来决定。

ThreadPoolExecutor 是 Java 中 Executor 框架的底层实现,它提供了一个可灵活配置的线程池。相比于 Executors 工具类提供的高级方法,ThreadPoolExecutor 允许你更详细地配置线程池的行为,包括核心线程数、最大线程数、线程存活时间、任务队列等。

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

public class CustomThreadPoolExecutorDemo {

    public static void main(String[] args) {
        // 创建 ThreadPoolExecutor
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2,  // 核心线程数
                5,  // 最大线程数
                1,  // 线程空闲时间
                TimeUnit.SECONDS,  // 时间单位
                new LinkedBlockingQueue<>(10)  // 任务队列
        );

        // 提交一些任务给线程池
        for (int i = 1; i <= 8; i++) {
            final int taskId = i;
            executor.submit(() -> {
                System.out.println("Task " + taskId + " is executing by " + Thread.currentThread().getName());
                try {
                    Thread.sleep(2000); // 模拟任务执行
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        // 关闭线程池
        executor.shutdown();
    }
}

通过 ThreadPoolExecutor 的构造方法自定义了线程池的各个参数,包括核心线程数、最大线程数、线程空闲时间、时间单位和任务队列。这种方式允许更灵活地配置线程池的行为,以适应不同的应用场景。

ThreadPoolExecutor 提供了更多的配置选项,例如可以指定拒绝策略、线程工厂等,从而满足更复杂的需求。需要注意的是,使用 ThreadPoolExecutor 需要更谨慎地处理线程池的参数,确保它们合理地配置以满足应用程序的性能和稳定性要求。

4.1 参数介绍

java 复制代码
public ThreadPoolExecutor(
    int corePoolSize,          // 核心线程数:线程池中始终保持存活的线程数,即使它们处于空闲状态。
    int maximumPoolSize,       // 最大线程数:线程池中允许的最大线程数。当队列满了并且当前线程数小于最大线程数时,会创建新的线程来处理任务。
    long keepAliveTime,        // 线程空闲时间:非核心线程的空闲时间超过这个时间,就会被回收。指定数值和时间单位一起使用。
    TimeUnit unit,             // 时间单位:指定 `keepAliveTime` 的时间单位,例如 TimeUnit.SECONDS。
    BlockingQueue<Runnable> workQueue,   // 任务队列:用于保存等待执行的任务的阻塞队列。可以选择不同类型的队列,如 LinkedBlockingQueue、ArrayBlockingQueue 等。
    ThreadFactory threadFactory,         // 线程工厂:用于创建新线程的工厂。可以通过它设置线程的名称、优先级、是否为守护线程等。
    RejectedExecutionHandler handler    // 拒绝策略:当任务无法被提交执行时,会使用该策略来处理。例如,可以选择默认的抛出异常、丢弃任务、调用调用者的线程来执行等。
)
  • 核心线程数(corePoolSize): 线程池中一直存活的线程数量,即使它们处于空闲状态。核心线程会一直存在,不会因为任务的执行完毕而被回收,除非设置了允许核心线程超时回收。
  • 最大线程数(maximumPoolSize): 线程池中允许的最大线程数量。当队列满了并且当前线程数小于最大线程数时,会创建新的线程来处理任务。
  • 线程空闲时间(keepAliveTime)和时间单位(unit): 非核心线程的空闲时间超过这个时间,就会被回收。指定数值和时间单位一起使用。
  • 任务队列(workQueue): 用于保存等待执行的任务的阻塞队列。不同类型的队列有不同的特性,如 LinkedBlockingQueue 是无界队列,而 ArrayBlockingQueue 是有界队列。
  • 线程工厂(threadFactory): 用于创建新线程的工厂,允许对线程的创建进行定制。可以设置线程的名称、优先级、是否为守护线程等。
  • 拒绝策略(handler): 当任务无法被提交执行时,会使用该策略来处理。例如,可以选择默认的抛出异常、丢弃任务、调用调用者的线程来执行等。

4.2 BlockingQueue <Runnable> (后面单独学习 内容较多)

BlockingQueue<Runnable> 是用于存储待执行任务的阻塞队列。Java 中提供了多种实现 BlockingQueue 接口的队列,其中常用的包括 LinkedBlockingQueueArrayBlockingQueueSynchronousQueue 等。下面简要介绍这三种队列:

  1. LinkedBlockingQueue: 使用链表实现的无界队列,理论上可以无限制地添加元素。在没有指定容量时,默认大小为 Integer.MAX_VALUE。适用于任务生产速度和消费速度差异较大的场景。

    java 复制代码
    // 无界队列
    BlockingQueue<Runnable> linkedBlockingQueue = new LinkedBlockingQueue<>();
  2. ArrayBlockingQueue: 使用数组实现的有界队列,需要指定队列的容量。适用于固定大小的线程池,可以防止资源耗尽。

    java 复制代码
    // 有界队列,指定容量为 10
    BlockingQueue<Runnable> arrayBlockingQueue = new ArrayBlockingQueue<>(10);
  3. SynchronousQueue: 一个不存储元素的队列。每个插入操作都必须等待另一个线程的对应移除操作,反之亦然。适用于任务提交和执行一一对应的场景,如直接将任务交给线程执行。

    java 复制代码
    // 不存储元素的队列
    BlockingQueue<Runnable> synchronousQueue = new SynchronousQueue<>();

这些队列的选择取决于应用程序的需求和性能特性。无界队列适合任务生产速度大于消费速度的场景,但可能导致内存耗尽。有界队列适合资源受限或任务生产和消费速度相近的场景。SynchronousQueue 适用于任务提交和执行一一对应的场景,但可能导致较高的线程创建和销毁开销。在选择队列时,需要根据具体的应用场景进行权衡。

4.3 方法shutdown()和shutdownNow()

shutdown()shutdownNow()ThreadPoolExecutor 中用于关闭线程池的两个方法。

4.3.1 shutdown()

shutdown() 方法用于优雅地关闭线程池。调用这个方法后,线程池将不再接受新的任务提交,但会继续执行已经提交的任务,直到所有任务完成。之后,线程池将关闭,释放占用的资源。

java 复制代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(...);

// 关闭线程池
executor.shutdown();

4.3.2 shutdownNow()

shutdownNow() 方法用于立即关闭线程池。调用这个方法后,线程池会尝试停止所有正在执行的任务,并返回等待执行的任务列表。该方法返回的列表包含所有未执行的任务,可能包括已经提交但未开始执行的任务以及正在执行的任务。

java 复制代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(...);

// 立即关闭线程池
List<Runnable> unexecutedTasks = executor.shutdownNow();
4.3.2.1 shutdownNow() 方法返回一个 List<Runnable>

shutdownNow() 方法返回一个 List<Runnable>,该列表包含尝试停止执行的所有未完成任务。具体来说,返回的列表包含以下两类任务:

  1. 已经提交但尚未开始执行的任务。
  2. 正在执行的任务,但被尝试停止。

返回的列表允许你查看在关闭线程池时未能正常完成的任务,可能包括部分或全部任务。这对于记录或处理这些未完成的任务信息是有用的。

以下是一个简单的示例,演示了如何使用 shutdownNow() 方法并检查未完成任务:

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

public class ShutdownNowExample {

    public static void main(String[] args) {
        // 创建一个线程池
        ExecutorService executor = Executors.newFixedThreadPool(2);

        // 提交一些任务给线程池
        for (int i = 1; i <= 5; i++) {
            final int taskId = i;
            executor.submit(() -> {
                System.out.println("Task " + taskId + " is executing by " + Thread.currentThread().getName());
                try {
                    Thread.sleep(5000); // 模拟任务执行,这里设置一个较长的执行时间
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        // 关闭线程池,获取未完成任务列表
        List<Runnable> uncompletedTasks = executor.shutdownNow();

        // 打印未完成任务的信息
        System.out.println("Number of uncompleted tasks: " + uncompletedTasks.size());
    }
}

在上面的例子中,线程池提交了5个任务,但由于设置了较长的任务执行时间,线程池在调用 shutdownNow() 方法时可能会有未完成的任务。通过检查返回的 List<Runnable>,我们可以获取未完成任务的信息,例如未完成任务的数量。

4.3.4 关系和注意事项

  • shutdown()shutdownNow() 都会停止线程池的接受新任务的能力。
  • shutdown() 是一种平缓的关闭方式,它等待正在执行的任务完成,然后关闭线程池。这个方法返回后,可以确保没有新任务被接受,且所有已提交的任务都已经执行完毕。
  • shutdownNow() 是一种立即关闭方式,它尝试停止所有正在执行的任务,返回未执行的任务列表。这个方法可能会导致一些任务未能执行完成。
  • 在某些情况下,shutdownNow() 返回的未执行任务列表可能为空。具体的行为取决于线程池的实现和任务执行的状态。
  • 使用这两个方法后,线程池将不能再接受新任务,如果需要重新使用线程池,需要重新创建一个新的线程池实例。

选择使用哪个方法取决于你的应用场景和关闭线程池的需求。 shutdown()shutdownNow() 以及线程的中断(interrupt)之间存在一些关联,下面分别解释它们的关系:

4.3.5 shutdown() 方法和中断:

  • shutdown() 方法不会直接中断线程,而是停止接受新任务,并等待已经提交的任务执行完成。这是一种平缓的关闭方式,不会强制终止线程。

  • 当调用 shutdown() 后,线程池会尝试执行所有已提交的任务,等待它们完成。如果某个任务在执行过程中被阻塞(如等待 I/O 操作、获取锁等),线程池将等待这些任务完成。因此,shutdown() 并不会中断线程,而是等待所有任务完成。

4.3.6 shutdownNow() 方法和中断:

  • shutdownNow() 方法尝试立即停止执行任务,并返回未完成的任务列表。这个方法会尝试中断正在执行的任务。如果任务的 run() 方法响应中断,任务会被中断;否则,任务可能继续执行。

  • 返回的未完成任务列表包含那些已经提交但未开始执行的任务以及尝试停止的正在执行的任务。对于正在执行的任务,如果任务未响应中断,它可能不会被完全中断。

4.3.7 中断和线程的终止:

  • 中断是一种线程间的协作机制,通过调用 Thread.interrupt() 方法来通知线程中断。线程可以通过检查中断状态来响应中断。

  • 如果线程池中的任务实现了对中断的响应,那么在线程池调用 shutdownNow() 时,尝试停止任务时可能会调用任务的 interrupt() 方法。

  • 线程池中的任务在执行时,可以通过检查 Thread.interrupted()isInterrupted() 方法来检测中断状态,并相应地处理中断请求。

总体来说,shutdown()shutdownNow() 并不直接中断线程,但线程池中的任务可以通过中断机制来响应关闭请求。在任务的执行中,要注意捕获中断异常并适当地处理中断请求。

4.3.8 isShutdown

isShutdown() 方法是 ExecutorService 接口的一个方法,用于判断线程池是否已经调用过 shutdown() 方法。具体来说,它用于检查线程池是否已经处于关闭状态。

java 复制代码
boolean isShutdown();

返回值是一个布尔值,如果线程池已经调用了 shutdown() 方法,返回 true;否则,返回 false

使用示例:

java 复制代码
ExecutorService executorService = Executors.newFixedThreadPool(5);

// ...

// 判断线程池是否已经关闭
if (executorService.isShutdown()) {
    System.out.println("The thread pool is already shut down.");
} else {
    System.out.println("The thread pool is still active.");
}

// ...

// 关闭线程池
executorService.shutdown();

在上面的例子中,通过调用 isShutdown() 方法,我们可以在关闭线程池之前检查它的状态。这在某些场景下可能是有用的,例如在执行关闭操作之前检查线程池是否已经关闭,以避免重复关闭的问题。

4.3.9 isTerminating()isTerminated()

isTerminating()isTerminated()ExecutorService 接口提供的方法,用于查询线程池的终止状态。

1. isTerminating()
java 复制代码
boolean isTerminating();

isTerminating() 方法用于判断线程池是否处于终止中。返回值是一个布尔值,如果线程池已经调用了 shutdown() 方法且至少有一个任务还在执行或等待执行,返回 true;否则,返回 false

2. isTerminated()
java 复制代码
boolean isTerminated();

isTerminated() 方法用于判断线程池是否已经完全终止。返回值是一个布尔值,如果线程池已经调用了 shutdown() 方法并且所有任务都已经执行完毕(包括已完成和正在执行的任务),返回 true;否则,返回 false

这两个方法可以用于监视线程池的终止状态。以下是一个示例:

java 复制代码
ExecutorService executorService = Executors.newFixedThreadPool(5);

// 提交一些任务给线程池
for (int i = 1; i <= 5; i++) {
    final int taskId = i;
    executorService.submit(() -> {
        System.out.println("Task " + taskId + " is executing by " + Thread.currentThread().getName());
        try {
            Thread.sleep(2000); // 模拟任务执行
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
}

// 关闭线程池
executorService.shutdown();

// 等待一段时间,让线程池执行任务
try {
    Thread.sleep(3000);
} catch (InterruptedException e) {
    e.printStackTrace();
}

// 判断线程池的终止状态
System.out.println("Terminating: " + executorService.isTerminating());
System.out.println("Terminated: " + executorService.isTerminated());

在上面的例子中,我们提交了一些任务给线程池,然后关闭线程池。在关闭后,通过 isTerminating()isTerminated() 方法来检查线程池的终止状态。在等待一段时间后,可以观察到 isTerminating() 返回 true,表示线程池正在终止中,而 isTerminated() 返回 false,表示线程池尚未完全终止。

4.3.10 awaitTermination(long timeout,TimeUnit unit)

awaitTermination(long timeout, TimeUnit unit)ExecutorService 接口提供的方法之一。它用于阻塞当前线程,等待线程池中的所有任务执行完毕,或者等待指定的超时时间。

java 复制代码
boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;
  • timeout:等待的超时时间。
  • unit:超时时间的单位。

这个方法返回一个布尔值,表示是否在指定的超时时间内线程池中的所有任务都已经执行完毕。如果在超时时间内任务执行完毕,返回 true;否则,返回 false

使用示例:

java 复制代码
ExecutorService executorService = Executors.newFixedThreadPool(5);

// 提交一些任务给线程池

// 关闭线程池
executorService.shutdown();

try {
    // 等待线程池中的任务执行完毕,或者超时时间到达
    if (executorService.awaitTermination(10, TimeUnit.SECONDS)) {
        System.out.println("All tasks have completed.");
    } else {
        System.out.println("Timeout reached. Some tasks might still be running.");
    }
} catch (InterruptedException e) {
    System.err.println("Interrupted while waiting for termination.");
    Thread.currentThread().interrupt(); // 重新设置中断状态
}

在这个例子中,awaitTermination(10, TimeUnit.SECONDS) 会阻塞当前线程,等待线程池中的所有任务执行完毕或者超过10秒超时时间。如果在超时时间内任务都执行完毕,返回 true;否则,返回 false

注意:

  • 在调用 shutdown() 后,建议使用 awaitTermination() 来等待任务的完成,以确保线程池已经完全关闭。
  • 如果在等待过程中当前线程被中断,awaitTermination() 方法会抛出 InterruptedException 异常,并需要适当处理中断状态。

4.3.11 工厂ThreadFactory+Thread+UncaughtExceptionHandler处理异常

可以通过实现 ThreadFactory 接口和设置 UncaughtExceptionHandler 来自定义线程工厂和处理线程异常的方式

以下是一个示例,演示如何使用 ThreadFactoryUncaughtExceptionHandler 处理线程异常:

java 复制代码
import java.util.concurrent.ThreadFactory;

public class CustomThreadFactoryExample {

    public static void main(String[] args) {
        // 创建自定义线程工厂
        ThreadFactory threadFactory = new CustomThreadFactory();

        // 使用自定义线程工厂创建线程
        Thread thread = threadFactory.newThread(() -> {
            System.out.println("Thread is running.");
            throw new RuntimeException("Simulated Exception");
        });

        // 设置线程的异常处理器
        thread.setUncaughtExceptionHandler((t, e) ->
                System.err.println("Uncaught exception in thread " + t.getName() + ": " + e.getMessage()));

        // 启动线程
        thread.start();
    }
}

class CustomThreadFactory implements ThreadFactory {

    @Override
    public Thread newThread(Runnable r) {
        // 创建线程时,设置线程的名称和异常处理器
        Thread thread = new Thread(r);
        thread.setName("CustomThread-" + thread.getId());
        thread.setUncaughtExceptionHandler((t, e) ->
                System.err.println("Uncaught exception in thread " + t.getName() + ": " + e.getMessage()));
        return thread;
    }
}

在这个例子中:

  1. 创建了一个实现了 ThreadFactory 接口的 CustomThreadFactory 类,用于创建新的线程。
  2. CustomThreadFactory 中,通过 newThread 方法创建新线程时,设置了线程的名称和异常处理器。
  3. 创建线程时,通过 thread.setUncaughtExceptionHandler 方法设置了线程的异常处理器。
  4. 线程运行时,当发生异常时,异常将由设置的异常处理器进行处理。

这样,通过自定义线程工厂和异常处理器,可以更灵活地处理线程的创建和异常情况。

4.3.12 set(get)RejectedExecutionHandler()

在 Java 的 ThreadPoolExecutor 中,可以使用 setRejectedExecutionHandler() 方法设置拒绝策略(RejectedExecutionHandler),而使用 getRejectedExecutionHandler() 方法获取当前线程池的拒绝策略。

1. setRejectedExecutionHandler() 方法
java 复制代码
void setRejectedExecutionHandler(RejectedExecutionHandler handler);

此方法用于设置线程池的拒绝策略。拒绝策略定义了当线程池无法接受新任务时的行为。常见的拒绝策略包括抛出异常、直接丢弃任务、将任务添加到调用线程中执行等。

示例:

java 复制代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(
        2,  // corePoolSize
        5,  // maximumPoolSize
        1,  // keepAliveTime
        TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(10) // workQueue
);

// 设置自定义的拒绝策略
executor.setRejectedExecutionHandler(new MyRejectedExecutionHandler());
2. getRejectedExecutionHandler() 方法
java 复制代码
RejectedExecutionHandler getRejectedExecutionHandler();

此方法用于获取当前线程池的拒绝策略。

示例:

java 复制代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(
        2,  // corePoolSize
        5,  // maximumPoolSize
        1,  // keepAliveTime
        TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(10) // workQueue
);

// 获取当前线程池的拒绝策略
RejectedExecutionHandler handler = executor.getRejectedExecutionHandler();

需要注意的是,当线程池处于关闭状态时,setRejectedExecutionHandler() 将抛出 RejectedExecutionException,因此最好在创建线程池后,但在调用 shutdown() 之前进行设置。

自定义拒绝策略需要实现 RejectedExecutionHandler 接口,例如:

java 复制代码
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;

public class MyRejectedExecutionHandler implements RejectedExecutionHandler {

    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        // 自定义拒绝策略的处理逻辑
        System.err.println("Task rejected: " + r.toString());
    }
}

在上述示例中,MyRejectedExecutionHandler 实现了 RejectedExecutionHandler 接口,当任务被拒绝时,会打印一条错误信息。您可以根据实际需求实现不同的拒绝策略。

4.3.13

ThreadPoolExecutor 中,allowsCoreThreadTimeOutallowCoreThreadTimeOut(boolean) 方法用于控制核心线程是否允许超时回收。

1. allowsCoreThreadTimeOut 方法
java 复制代码
boolean allowsCoreThreadTimeOut();

此方法用于查询线程池是否允许核心线程超时回收。如果返回 true,则核心线程在空闲时会根据 keepAliveTime 进行超时回收。如果返回 false,则核心线程将一直保持存活,不会超时回收。

2. allowCoreThreadTimeOut(boolean) 方法
java 复制代码
void allowCoreThreadTimeOut(boolean value);

此方法用于设置线程池是否允许核心线程超时回收。如果参数 valuetrue,则允许核心线程超时回收;如果为 false,则不允许核心线程超时回收。

示例:

java 复制代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(
        2,  // corePoolSize
        5,  // maximumPoolSize
        1,  // keepAliveTime
        TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(10) // workQueue
);

// 查询线程池是否允许核心线程超时回收
boolean allowsCoreThreadTimeOut = executor.allowsCoreThreadTimeOut();

// 设置线程池允许核心线程超时回收
executor.allowCoreThreadTimeOut(true);

在上述示例中,通过 allowsCoreThreadTimeOut() 可以查询线程池是否允许核心线程超时回收,而通过 allowCoreThreadTimeOut(true) 可以设置线程池允许核心线程超时回收。

核心线程超时回收的典型应用场景是,在任务提交量较低且需要节省资源时,可以允许核心线程在空闲一段时间后自动回收,以减少线程池的资源占用。

4.3.14 prestartCoreThread()prestartAllCoreThreads()

ThreadPoolExecutor 中,prestartCoreThread()prestartAllCoreThreads() 方法用于预启动核心线程,即在启动线程池时创建一些核心线程,以便更早地响应任务的执行。

1. prestartCoreThread() 方法
java 复制代码
boolean prestartCoreThread();

此方法用于尝试启动一个核心线程。如果成功启动了一个核心线程,则返回 true;如果无法启动核心线程(例如,因为线程数已达到核心线程数上限),则返回 false

示例:

java 复制代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(
        2,  // corePoolSize
        5,  // maximumPoolSize
        1,  // keepAliveTime
        TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(10) // workQueue
);

// 尝试启动一个核心线程
boolean started = executor.prestartCoreThread();
2. prestartAllCoreThreads() 方法
java 复制代码
int prestartAllCoreThreads();

此方法用于启动所有未启动的核心线程,并返回启动的线程数量。如果线程池中的核心线程已经启动,或者由于某些原因无法启动,则返回 0

示例:

java 复制代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(
        2,  // corePoolSize
        5,  // maximumPoolSize
        1,  // keepAliveTime
        TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(10) // workQueue
);

// 启动所有未启动的核心线程
int startedThreads = executor.prestartAllCoreThreads();

在上述示例中,通过 prestartCoreThread() 可以尝试启动一个核心线程,而通过 prestartAllCoreThreads() 可以启动所有未启动的核心线程。

这两个方法可以在创建线程池后手动预启动一些核心线程,以提高线程池对任务的响应速度。

4.3.15 afterExecute()和beforeExecute()

ThreadPoolExecutor 中,beforeExecute(Thread t, Runnable r)afterExecute(Runnable r, Throwable t) 是两个钩子方法(hook methods),它们允许你在任务执行前后执行一些额外的操作。

1. beforeExecute(Thread t, Runnable r)
java 复制代码
protected void beforeExecute(Thread t, Runnable r) {
    // 在任务执行前执行的操作
}

此方法在执行每个任务之前被调用。它提供了执行任务的线程和要执行的任务作为参数。你可以通过重写这个方法来执行一些初始化或日志记录等操作。

java 复制代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(
        corePoolSize,
        maximumPoolSize,
        keepAliveTime,
        TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(capacity)
) {
    @Override
    protected void beforeExecute(Thread t, Runnable r) {
        super.beforeExecute(t, r);
        // 在任务执行前执行的操作
        System.out.println("Before executing task: " + r.toString());
    }
};
2. afterExecute(Runnable r, Throwable t)
java 复制代码
protected void afterExecute(Runnable r, Throwable t) {
    // 在任务执行后执行的操作
}

此方法在任务执行完成后被调用。它提供了执行完的任务和可能的异常作为参数。你可以通过重写这个方法来执行一些收尾工作,例如资源清理、日志记录、性能统计等操作。

java 复制代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(
        corePoolSize,
        maximumPoolSize,
        keepAliveTime,
        TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(capacity)
) {
    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        // 在任务执行后执行的操作
        System.out.println("After executing task: " + r.toString());
        if (t != null) {
            System.err.println("Task ended with an exception: " + t.getMessage());
        }
    }
};

通过这两个钩子方法,你可以在任务执行前后插入自定义逻辑,对线程池的行为进行定制化。这可以用于调试、性能监控、异常处理等方面。注意,这两个方法是在任务执行的同一线程中调用的,因此对线程安全性要有一定的考虑。

4.3.16 remove(Runnable)

ThreadPoolExecutor 中,remove(Runnable) 方法用于尝试从工作队列中移除一个指定的任务,而不是从正在执行的任务中移除。这个方法可以用于取消等待执行的任务。

java 复制代码
boolean remove(Runnable task);
  • task:要从队列中移除的任务。

返回值为 true 表示成功从队列中移除任务,false 表示任务不在队列中或已经被执行。

使用 remove(Runnable) 方法时需要注意以下几点:

  1. 只能从队列中移除还未开始执行的任务。 如果任务已经在执行,remove(Runnable) 方法将返回 false

  2. 不会中断正在执行的任务。 即使成功从队列中移除了任务,正在执行的任务仍会继续执行。

下面是一个简单的示例:

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

public class ThreadPoolRemoveExample {

    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2,  // corePoolSize
                5,  // maximumPoolSize
                1,  // keepAliveTime
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(10) // workQueue
        );

        // 提交任务
        Runnable task1 = () -> System.out.println("Task 1 is executing.");
        Runnable task2 = () -> {
            try {
                Thread.sleep(5000);  // 模拟长时间任务
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Task 2 is executing.");
        };

        executor.submit(task1);
        Future<?> future = executor.submit(task2);

        // 移除等待执行的任务
        boolean removed = executor.remove(task1);
        System.out.println("Task 1 removed: " + removed);

        // 移除正在执行的任务(不会中断执行中的任务)
        removed = executor.remove(task2);
        System.out.println("Task 2 removed: " + removed);

        // 等待任务2执行完毕
        try {
            future.get();
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }

        // 关闭线程池
        executor.shutdown();
    }
}

在上述示例中,任务1被成功移除,因为它还未开始执行。而任务2在执行中,即使调用了 remove() 方法,任务2仍会继续执行,不会被中断。这强调了 remove(Runnable) 方法的一些限制。

4.3.17 ThreadPoolExecutor的拒绝策略

ThreadPoolExecutor 在面临无法接受新任务的情况时,会采取拒绝策略(RejectedExecutionHandler)。拒绝策略定义了在线程池达到最大容量且工作队列已满时,应该采取的行为。以下是 ThreadPoolExecutor 提供的几种拒绝策略:

1. AbortPolicy(默认策略)

java 复制代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(
        corePoolSize,
        maximumPoolSize,
        keepAliveTime,
        TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(capacity),
        new ThreadPoolExecutor.AbortPolicy()
);

AbortPolicy 是默认的拒绝策略。当工作队列已满且线程池达到最大容量时,新任务将被直接拒绝,并抛出 RejectedExecutionException 异常。

2. CallerRunsPolicy

java 复制代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(
        corePoolSize,
        maximumPoolSize,
        keepAliveTime,
        TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(capacity),
        new ThreadPoolExecutor.CallerRunsPolicy()
);

CallerRunsPolicy 策略会直接在调用者线程中执行被拒绝的任务。这意味着当线程池无法接受新任务时,任务将在调用 execute 的线程中执行。

3. DiscardPolicy

java 复制代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(
        corePoolSize,
        maximumPoolSize,
        keepAliveTime,
        TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(capacity),
        new ThreadPoolExecutor.DiscardPolicy()
);

DiscardPolicy 策略会默默地丢弃被拒绝的任务,不会抛出异常也不会执行任务。这可能导致某些任务被忽略。

4. DiscardOldestPolicy

java 复制代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(
        corePoolSize,
        maximumPoolSize,
        keepAliveTime,
        TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(capacity),
        new ThreadPoolExecutor.DiscardOldestPolicy()
);

DiscardOldestPolicy 策略会丢弃队列中等待时间最长的任务,然后尝试将新任务加入队列。

自定义拒绝策略

除了上述几种内置的拒绝策略,你还可以通过实现 RejectedExecutionHandler 接口来自定义拒绝策略。例如:

java 复制代码
public class CustomRejectedExecutionHandler implements RejectedExecutionHandler {

    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        // 自定义的拒绝策略逻辑
        System.err.println("Task rejected: " + r.toString());
    }
}

然后在创建线程池时使用自定义的拒绝策略:

java 复制代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(
        corePoolSize,
        maximumPoolSize,
        keepAliveTime,
        TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(capacity),
        new CustomRejectedExecutionHandler()
);

通过选择适当的拒绝策略,可以更好地应对线程池无法接受新任务的情况。

相关推荐
CC数分6 小时前
2026年HRBP岗位硬性要求和加分项
面试·职场和发展·数据分析
2601_962680226 小时前
数据分析面试常考的SQL题和业务题分别是哪几类?
sql·面试·数据分析
黄敬峰7 小时前
一文搞懂 Agentic RAG:用 LangGraph 打造会思考的智能检索架构
面试
晚安日记wanna8 小时前
SSR 为什么不适合登录态从水合冲突到缓存串号
前端·react.js·面试
晚安日记wanna8 小时前
压测 TPS 卡在 800CPU 只跑四成六步定位法
运维·面试·测试
晚安日记wanna8 小时前
批量请求失败只弹一个 Toast面试官想听五层
前端·面试·架构
晚安日记wanna8 小时前
TS const 类型参数一道面试题的四层追问
前端·面试·typescript
晚安日记wanna8 小时前
大表 DDL 面试翻车现场Online DDL 为什么还会锁死业务
数据库·面试·架构
晚安日记wanna8 小时前
MySQL 主从延迟别只答并行复制
数据库·面试·架构
SamDeepThinking8 小时前
Java 17内存屏障:为什么需要,怎么用
java·后端·面试