重温JAVA线程池精髓:Executor、ExecutorService及Executors的源码剖析与应用指南

简介: 重温JAVA线程池精髓:Executor、ExecutorService及Executors的源码剖析与应用指南

1️⃣引言

在Java并发编程中,线程池是一个非常重要的概念。它可以帮助我们更好地管理和控制线程的使用,避免因为大量线程的创建和销毁带来的性能开销。Java的java.util.concurrent(简称JUC)包

中提供了一套丰富的线程池工具,包括Executor接口、ExecutorService接口以及Executors工厂类等。本文将详细介绍这些工具的使用和原理,帮助大家更好地理解和应用Java中的线程池技术。

2️⃣Executor接口

Executor接口是JUC包中定义的一个执行器接口,它只有一个execute方法,接收一个Runnable对象作为参数,并执行Runnable中的操作。这个接口非常简单,但它定义了执行器的基本功能。

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

在实际应用中,我们通常不会直接使用Executor接口,而是使用它的子接口ExecutorService,它提供了更丰富的功能。

3️⃣ExecutorService接口

ExecutorService接口继承自Executor接口,并增加了关于执行器服务的定义。它提供了一系列的方法,包括关闭执行器、立即关闭、检查执行器是否关闭、等待任务终止、提交有返回值的任务以及批量提交任务等。

public interface ExecutorService extends Executor {
    void shutdown();
    List<Runnable> shutdownNow();
    boolean isShutdown();
    boolean isTerminated();
    boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;
    <T> Future<T> submit(Callable<T> task);
    <T> Future<T> submit(Runnable task, T result);
    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) throws InterruptedException, ExecutionException;
    <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}

这些方法的具体含义和使用方式如下:

  • shutdown():关闭执行器,已提交的任务会继续执行,但不接受新的任务。
  • shutdownNow():立即关闭执行器,尝试停止所有正在执行的任务,并返回等待执行的任务列表。
  • isShutdown():检查执行器是否已关闭。
  • isTerminated():检查执行器是否已终止,即所有任务都已完成。
  • awaitTermination(long timeout, TimeUnit unit):等待任务终止,如果超过指定时间则返回false。
  • submit(Callable<T> task):提交一个有返回值的任务,并返回一个Future对象,通过该对象可以查看任务执行是否完成,并获取返回值。
  • submit(Runnable task, T result):提交一个Runnable任务和一个结果值,当任务执行完成后,返回该结果值。注意这个结果值是在任务执行前就确定的,与任务的实际执行结果无关。如果希望获取任务的实际执行结果,应该使用Callable任务。
  • submit(Runnable task):提交一个Runnable任务,并返回一个Future对象。由于Runnable任务没有返回值,所以这个Future对象的get方法将返回null。这个方法主要用于将Runnable任务转换为Future对象,以便使用Future的相关功能(如取消任务、检查任务是否完成等)。但这个用法并不常见,因为Runnable任务本身就不支持返回值。更常见的做法是直接使用execute(Runnable command)方法执行Runnable任务。
  • invokeAll(Collection<? extends Callable<T>> tasks):批量提交Callable任务,并返回一个Future对象的列表。当所有任务都完成后,可以通过这些Future对象获取任务的返回值。如果某个任务执行失败,那么对应的Future对象的get方法将抛出ExecutionException异常。这个方法会等待所有任务都完成后才返回。如果希望设置超时时间,可以使用另一个重载版本的方法。
  • invokeAny(Collection<? extends Callable<T>> tasks):批量提交Callable任务,并返回第一个成功完成的任务的返回值。当找到第一个成功完成的任务后,该方法会立即返回,而不会等待其他任务完成。如果所有任务都失败,那么该方法将抛出ExecutionException异常。这个方法通常用于实现“多个路径中选择一个最快路径”的场景。同样地,这个方法也有一个设置超时时间的重载版本。

需要注意的是,虽然ExecutorService接口提供了很多功能强大的方法,但我们在实际使用中并不需要记住所有这些方法。大部分情况下,我们只需要关注几个常用的方法就足够了,比如execute()、submit()和shutdown()等。其他的方法可以在需要时查阅文档或参考资料。

4️⃣Executors工厂类

Executors是一个工厂类,它提供了一系列静态方法来创建不同类型的线程池。这些线程池都是ExecutorService接口的实现类。通过Executors的工厂方法,我们可以非常方便地创建和管理线程池。下面介绍几种常见的线程池类型:

3.1. FixedThreadPool

  • 固定大小的线程池。创建时指定线程池的大小,当有新任务提交时,如果线程池中有空闲线程,则使用空闲线程执行任务;
  • 如果没有空闲线程,则新任务会等待直到有线程空闲出来。这种线程池适用于已知并发压力的情况下,对线程数做限制,避免由于大量线程的创建和销毁带来的性能开销。
  • 使用:ExecutorService executor = Executors.newFixedThreadPool(10); 创建一个大小为10的固定线程池。

3.2. WorkStealingPool

  • 拥有多个任务队列的线程池(在ForkJoinPool中实现)。这种线程池可以减少线程间的竞争和上下文切换开销,提高处理器的利用率。
  • 它适用于大量异步任务的场景,如并行计算、大数据处理等。

3.3. SingleThreadExecutor

  • 单线程执行器。顾名思义,这种线程池中只有一个线程执行任务。
  • 所有提交的任务都会按照提交的顺序依次执行。
  • 这种线程池适用于需要保证任务执行顺序的场景,如日志记录、事件驱动等。
  • 使用:ExecutorService executor = Executors.newSingleThreadExecutor(); 创建一个单线程执行器。

3.4. CachedThreadPool

  • 可缓存的线程池,这种线程池会根据需要创建线程来执行任务,并且可以重复利用已存在的线程来执行新的任务。
  • 当线程池中的线程在一定时间内没有执行任务时,它会被自动销毁以释放资源。
  • 这种线程池适用于并发压力较大且任务执行时间较短的场景,如Web服务器处理HTTP请求等。
  • 使用:ExecutorService executor = Executors.newCachedThreadPool(); 创建一个可缓存的线程池。
  • 但需要注意的是,在实际应用中我们可能需要更加谨慎地使用CachedThreadPool,因为如果不当使用可能会导致系统资源耗尽(如创建过- 多的线程导致内存溢出等)。因此在使用CachedThreadPool时需要特别关注任务的执行时间和数量以及系统的资源状况等因素。

3.5. SingleThreadScheduledExecutor和 ScheduledThreadPool

  • 这两种线程池都支持定时或周期性执行任务。
  • SingleThreadScheduledExecutor是一个单线程的定时任务执行器,它按照任务提交的顺序依次执行定时任务或周期性任务;
  • 而ScheduledThreadPool是一个可以指定线程数量的定时任务执行器,它可以同时执行多个定时任务或周期性任务。
  • 两种线程池适用于需要定时触发或周期性触发的场景,如定时发送邮件、定时更新缓存等。
  • 使用:ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); 或 ScheduledExecutorService executor = Executors.newScheduledThreadPool(10); 分别创建一个单线程定时任务执行器和一个大小为10的定时任务执行器。

3.6 注意事项

需要注意的是,虽然Executors工厂类提供了很多方便的静态方法来创建线程池,但在实际使用中我们也需要关注线程池的配置和管理问题。

  • 例如我们需要根据实际的应用场景和需求来选择合适的线程池类型和大小;
  • 我们需要在不再需要线程池时及时关闭它以释放资源;
  • 我们还需要关注线程池中的任务执行情况和异常处理等。

只有合理地配置和管理线程池,我们才能充分发挥它的优势并提高系统的性能和稳定性。

5️⃣探讨一个问题:线程池的优雅关闭

线程池的优雅关闭指的是在不再需要线程池时,能够平滑地终止其执行,释放相关资源,并确保正在执行的任务能够完成或得到妥善处理。我们使用ExecutorService接口提供的关闭方法可以实现线程池的优雅关闭。

下面是实现线程池优雅关闭的一般步骤:

  1. 调用shutdown()方法:首先调用ExecutorServiceshutdown()方法,它将启动线程池的关闭过程。此时,线程池不再接受新任务的提交,但会继续处理队列中等待的任务。
  2. 等待任务完成:接着,可以使用awaitTermination方法来等待线程池中所有任务都执行完毕。这个方法接受两个参数:超时时间和时间单位。如果在指定的超时时间内所有任务都执行完毕,则方法返回true;否则返回false。可以根据需要设置合适的超时时间。
  3. 处理未完成任务(可选):如果在等待超时后仍有任务未执行完毕,可以选择调用shutdownNow()方法来尝试立即停止所有正在执行的任务,并返回队列中等待执行的任务列表。然后,可以对这些未完成的任务进行补救操作,如记录日志、重新提交到另一个线程池等。但请注意,shutdownNow()方法并不保证能立即停止所有任务,因为线程的执行是由操作系统调度的。
  4. 检查线程池状态:最后,可以检查线程池的状态来确保它已经完全关闭。可以使用isTerminated()方法来检查线程池是否已关闭且所有任务都已完成。如果返回true,则表示线程池已成功关闭;否则,可能需要进一步处理未完成的任务或检查线程池的配置。

代码如下:

ExecutorService executorService = Executors.newFixedThreadPool(10);
// 提交任务到线程池...

// 启动关闭过程
executorService.shutdown();
try {
    // 等待任务完成,这里设置超时时间为60秒
    if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
        // 超时后仍有任务未执行完毕,可以选择强制关闭
        List<Runnable> pendingTasks = executorService.shutdownNow();
        // 处理未完成的任务...
    }
} catch (InterruptedException e) {
    // 处理中断异常...
    executorService.shutdownNow(); // 保留中断状态
    // 再次检查线程池状态和处理未完成的任务...
} finally {
    if (!executorService.isTerminated()) {
        // 线程池未正常关闭,记录日志或进行其他处理...
    }
}

通过上述步骤,可以实现线程池的优雅关闭,确保资源的正确释放和任务的妥善处理。

6️⃣结语

总之,Executor、ExecutorService接口和Executors工厂类共同构成了Java中强大而灵活的线程池框架。

  • Executor接口定义了执行任务的基本行为,它是线程池框架的基石。
  • 而ExecutorService接口则扩展了Executor的功能,提供了一系列丰富的方法来管理和控制任务的执行。
  • 最后,Executors工厂类为开发者提供了创建各种类型线程池的便捷途径,无需手动实现复杂的线程池逻辑。

它们允许以简单而高效的方式管理和控制并发任务的执行,提高了系统的性能和可伸缩性。在使用线程池时,建议根据具体的应用场景和需求选择合适的线程池类型,并注意正确地管理线程池的生命周期和任务提交。


相关文章
|
1月前
|
存储 数据可视化 Java
【Java】Java swing 民宿管理系统 GUI(源码+可视化界面)【独一无二】
【Java】Java swing 民宿管理系统 GUI(源码+可视化界面)【独一无二】
|
17天前
|
Java 调度 数据库
Java并发编程:深入理解线程池
在Java并发编程的海洋中,线程池是一艘强大的船,它不仅提高了性能,还简化了代码结构。本文将带你潜入线程池的深海,探索其核心组件、工作原理及如何高效利用线程池来优化你的并发应用。
|
17天前
|
存储 监控 Java
Java多线程优化:提高线程池性能的技巧与实践
Java多线程优化:提高线程池性能的技巧与实践
44 1
|
30天前
|
安全 Java 数据库
一天十道Java面试题----第四天(线程池复用的原理------>spring事务的实现方式原理以及隔离级别)
这篇文章是关于Java面试题的笔记,涵盖了线程池复用原理、Spring框架基础、AOP和IOC概念、Bean生命周期和作用域、单例Bean的线程安全性、Spring中使用的设计模式、以及Spring事务的实现方式和隔离级别等知识点。
|
30天前
|
存储 监控 安全
一天十道Java面试题----第三天(对线程安全的理解------>线程池中阻塞队列的作用)
这篇文章是Java面试第三天的笔记,讨论了线程安全、Thread与Runnable的区别、守护线程、ThreadLocal原理及内存泄漏问题、并发并行串行的概念、并发三大特性、线程池的使用原因和解释、线程池处理流程,以及线程池中阻塞队列的作用和设计考虑。
|
13天前
|
Kubernetes jenkins 持续交付
从代码到k8s部署应有尽有系列-java源码之String详解
本文详细介绍了一个基于 `gitlab + jenkins + harbor + k8s` 的自动化部署环境搭建流程。其中,`gitlab` 用于代码托管和 CI,`jenkins` 负责 CD 发布,`harbor` 作为镜像仓库,而 `k8s` 则用于运行服务。文章具体介绍了每项工具的部署步骤,并提供了详细的配置信息和示例代码。此外,还特别指出中间件(如 MySQL、Redis 等)应部署在 K8s 之外,以确保服务稳定性和独立性。通过本文,读者可以学习如何在本地环境中搭建一套完整的自动化部署系统。
40 0
|
3天前
|
Java 调度 开发者
Java并发编程:深入理解线程池
在Java的世界中,线程池是提升应用性能、实现高效并发处理的关键工具。本文将深入浅出地介绍线程池的核心概念、工作原理以及如何在实际应用中有效利用线程池来优化资源管理和任务调度。通过本文的学习,读者能够掌握线程池的基本使用技巧,并理解其背后的设计哲学。
|
3天前
|
缓存 监控 Java
Java中的并发编程:理解并应用线程池
在Java的并发编程中,线程池是提高应用程序性能的关键工具。本文将深入探讨如何有效利用线程池来管理资源、提升效率和简化代码结构。我们将从基础概念出发,逐步介绍线程池的配置、使用场景以及最佳实践,帮助开发者更好地掌握并发编程的核心技巧。
|
4天前
|
缓存 监控 Java
java中线程池的使用
java中线程池的使用
|
20天前
|
缓存 Java 调度
【Java 并发秘籍】线程池大作战:揭秘 JDK 中的线程池家族!
【8月更文挑战第24天】Java的并发库提供多种线程池以应对不同的多线程编程需求。本文通过实例介绍了四种主要线程池:固定大小线程池、可缓存线程池、单一线程线程池及定时任务线程池。固定大小线程池通过预设线程数管理任务队列;可缓存线程池能根据需要动态调整线程数量;单一线程线程池确保任务顺序执行;定时任务线程池支持周期性或延时任务调度。了解并正确选用这些线程池有助于提高程序效率和资源利用率。
31 2