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

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


相关文章
|
2月前
|
安全 Java 开发者
告别NullPointerException:掌握Java Optional的精髓
告别NullPointerException:掌握Java Optional的精髓
|
7月前
|
安全 Java 调度
Netty源码—3.Reactor线程模型二
本文主要介绍了NioEventLoop的执行总体框架、Reactor线程执行一次事件轮询、Reactor线程处理产生IO事件的Channel、Reactor线程处理任务队列之添加任务、Reactor线程处理任务队列之执行任务、NioEventLoop总结。
|
7月前
|
安全 Java
Netty源码—2.Reactor线程模型一
本文主要介绍了关于NioEventLoop的问题整理、理解Reactor线程模型主要分三部分、NioEventLoop的创建和NioEventLoop的启动。
|
8月前
|
Java 中间件 调度
【源码】【Java并发】从InheritableThreadLocal和TTL源码的角度来看父子线程传递
本文涉及InheritableThreadLocal和TTL,从源码的角度,分别分析它们是怎么实现父子线程传递的。建议先了解ThreadLocal。
293 4
【源码】【Java并发】从InheritableThreadLocal和TTL源码的角度来看父子线程传递
|
9月前
|
缓存 Java
线程池初始化严禁使用Executors
线程池初始化严禁使用Executors
|
9月前
|
Java 调度
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
当我们创建一个`ThreadPoolExecutor`的时候,你是否会好奇🤔,它到底发生了什么?比如:我传的拒绝策略、线程工厂是啥时候被使用的? 核心线程数是个啥?最大线程数和它又有什么关系?线程池,它是怎么调度,我们传入的线程?...不要着急,小手手点上关注、点赞、收藏。主播马上从源码的角度带你们探索神秘线程池的世界...
450 0
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
|
缓存 监控 Java
Java线程池提交任务流程底层源码与源码解析
【11月更文挑战第30天】嘿,各位技术爱好者们,今天咱们来聊聊Java线程池提交任务的底层源码与源码解析。作为一个资深的Java开发者,我相信你一定对线程池并不陌生。线程池作为并发编程中的一大利器,其重要性不言而喻。今天,我将以对话的方式,带你一步步深入线程池的奥秘,从概述到功能点,再到背景和业务点,最后到底层原理和示例,让你对线程池有一个全新的认识。
249 12
|
存储 运维 API
源码解密协程队列和线程队列的实现原理(一)
源码解密协程队列和线程队列的实现原理(一)
271 1
|
存储 安全 API
源码解密协程队列和线程队列的实现原理(二)
源码解密协程队列和线程队列的实现原理(二)
136 1
|
存储 缓存 Java
什么是线程池?从底层源码入手,深度解析线程池的工作原理
本文从底层源码入手,深度解析ThreadPoolExecutor底层源码,包括其核心字段、内部类和重要方法,另外对Executors工具类下的四种自带线程池源码进行解释。 阅读本文后,可以对线程池的工作原理、七大参数、生命周期、拒绝策略等内容拥有更深入的认识。
1710 31
什么是线程池?从底层源码入手,深度解析线程池的工作原理

热门文章

最新文章