重温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工厂类为开发者提供了创建各种类型线程池的便捷途径,无需手动实现复杂的线程池逻辑。

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


相关文章
|
21天前
|
缓存 Java 开发者
Java多线程并发编程:同步机制与实践应用
本文深入探讨Java多线程中的同步机制,分析了多线程并发带来的数据不一致等问题,详细介绍了`synchronized`关键字、`ReentrantLock`显式锁及`ReentrantReadWriteLock`读写锁的应用,结合代码示例展示了如何有效解决竞态条件,提升程序性能与稳定性。
78 6
|
20天前
|
监控 Java 数据库连接
Java线程管理:守护线程与用户线程的区分与应用
在Java多线程编程中,线程可以分为守护线程(Daemon Thread)和用户线程(User Thread)。这两种线程在行为和用途上有着明显的区别,了解它们的差异对于编写高效、稳定的并发程序至关重要。
28 2
|
25天前
|
数据采集 存储 数据处理
Python中的多线程编程及其在数据处理中的应用
本文深入探讨了Python中多线程编程的概念、原理和实现方法,并详细介绍了其在数据处理领域的应用。通过对比单线程与多线程的性能差异,展示了多线程编程在提升程序运行效率方面的显著优势。文章还提供了实际案例,帮助读者更好地理解和掌握多线程编程技术。
|
25天前
|
存储 监控 安全
深入理解ThreadLocal:线程局部变量的机制与应用
在Java的多线程编程中,`ThreadLocal`变量提供了一种线程安全的解决方案,允许每个线程拥有自己的变量副本,从而避免了线程间的数据竞争。本文将深入探讨`ThreadLocal`的工作原理、使用方法以及在实际开发中的应用场景。
49 2
|
1月前
|
安全 Java 开发者
Java 多线程并发控制:深入理解与实战应用
《Java多线程并发控制:深入理解与实战应用》一书详细解析了Java多线程编程的核心概念、并发控制技术及其实战技巧,适合Java开发者深入学习和实践参考。
52 6
|
29天前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
2月前
|
存储 并行计算 安全
C++多线程应用
【10月更文挑战第29天】C++ 中的多线程应用广泛,常见场景包括并行计算、网络编程中的并发服务器和图形用户界面(GUI)应用。通过多线程可以显著提升计算速度和响应能力。示例代码展示了如何使用 `pthread` 库创建和管理线程。注意事项包括数据同步与互斥、线程间通信和线程安全的类设计,以确保程序的正确性和稳定性。
|
2月前
|
监控 Java
在实际应用中选择线程异常捕获方法的考量
【10月更文挑战第15天】选择最适合的线程异常捕获方法需要综合考虑多种因素。没有一种方法是绝对最优的,需要根据具体情况进行权衡和选择。在实际应用中,还需要不断地实践和总结经验,以提高异常处理的效果和程序的稳定性。
30 3
|
2月前
|
调度 Android开发 开发者
构建高效Android应用:探究Kotlin多线程优化策略
【10月更文挑战第11天】本文探讨了如何在Kotlin中实现高效的多线程方案,特别是在Android应用开发中。通过介绍Kotlin协程的基础知识、异步数据加载的实际案例,以及合理使用不同调度器的方法,帮助开发者提升应用性能和用户体验。
64 4
|
2月前
|
数据采集 存储 Java
Crawler4j在多线程网页抓取中的应用
Crawler4j在多线程网页抓取中的应用
下一篇
DataWorks