Java 并发编程 Executor 框架

两级调度模型

在 HotSpot VM 的线程模型中,Java 线程被一对一映射为本地操作系统线程。在上层,Java 多线程程序通常应用分解成若干个任务,然后使用用户级的调度器(Executor)将这些任务映射为固定数量的线程;在底层,操作系统内核将这些线程映射到硬件处理器。这种两级调度模型的示意图如图所示:

从图中可以看出,应用程序通过 Executor 框架控制上层调度,下层的调度则由操作系统内核控制


Executor 框架

之前我们学习过使用 ThreadPoolExecutor 创建线程池,实际上 ThreadPoolExecutor 就是属于 Executor 框架的一部分。从两级调度模型我们看出,Executor 框架介于用户需要执行的任务和用于执行任务的线程池之间,可以将 Executor 框架理解为 JDK 提供的一组并发编程规范

根据两级调度模型,我们可以得知 Executor 框架主要由三大部分组成:

  • 任务

    被执行任务,需要实现Runnable 接口或 Callable 接口

  • 任务的执行

    包括任务执行机制的核心接口 Executor,以及继承自 Executor 的 ExecutorService 接口。Executor 框架有两个关键类实现了 ExecutorService 接口,分别是 ThreadPoolExecutor 和 ScheduleThreadPoolExecutor,它们都是线程池的实现类,用于执行被提交的任务

  • 异步计算的结果

    任务执行完成需要获取结果,需要用到接口 Future 和实现 Future 接口的 FutureTask 类

我们来看一下使用 Executor 框架的过程:首先要创建实现 Runnable 或 Callable 接口的任务对象,可以手动实现,也可以使用工具类 Executors 把一个 Runnable 对象封装为一个 Callable 对象

// 返回结果为 null
Executors.callable(Runnable task);
// 返回结果为 result
Executors.callable(Runnable task, T result);

然后把 Runnable 对象直接交给实现了 ExecutorService 接口的线程池执行

// 假设此处 executorService 为 ThreadPoolExecutor
executorService.execute(Runnable command);
executorService.submit(Runnable task);
executorService.submit(Callable<T> task);

如果执行 submit 方法,将会返回一个实现 Future 接口的对象 FutureTask。最后,主线程可以执行 FutureTask.get() 方法来等待任务执行完成,也可以执行 FutureTask.cancel(boolean mayInterruptIfRunning) 来取消此任务的执行

补充一张 Executor 框架的成员及其关系图


ThreadPoolExecutor

Executor 框架最核心的类是 ThreadPoolExecutor,它是线程池的实现类,下面分别介绍三种 ThreadPoolExecutor,它们都可以使用工具类 Executors 直接创建

1. FixedThreadPool

FixedThreadPool 被称为可重用固定线程数的线程池,下面是 FixedThreadPool 的源代码实现

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

FixedThreadPool 的 corePoolSize 和 maximumPoolSize 都被设置为创建 FixedThreadPool 时指定的参数 nThreads。当线程池中的线程数大于 corePoolSize 时,keepAliveTime 为多余的空闲线程等待新任务的最长时间,超过这个时间后多余的线程将被终止。这里把 keepAliveTime 设置为 0L,意味着多余的空闲线程会被立即终止

FixedThreadPool 使用无界队列 LinkedBlockingQueue 作为线程池的工作队列(队列的容量为 Integer.MAX_VALUE),使用无界队列作为工作队列会对线程池带来如下影响:当线程池中的线程数达到 corePoolSize 后,新任务将在无界队列中等待,而无界队列几乎可以容纳无限多的新任务,因此线程池中的线程数永远不会超过 corePoolSize,因此 maximumPoolSize 就成了无效参数,keepAliveTime 也是无效参数,运行中的 FixThreadPool 不会拒绝任务

2. SingleThreadExecutor

SingleThreadExecutor 是使用单个线程的 Executor,下面是 SingleThreadExecutor 的源代码实现

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

SingleThreadExecutor 的 corePoolSize 和 maximumPoolSize 被设置为 1,其他参数与 FixedThreadPool 相同。SingleThreadExecutor 使用无界队列 LinkedBlockingQueue 作为线程池的工作队列,其带来的影响与 FixedThreadPool 相同,这里就不再赘述了

3. CachedThreadPool

CachedThreadPool 是一个会根据需要创建新线程的线程池,下面是创建 CachedThreadPool 的源代码

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

CachedThreadPool 的 corePoolSize 被设置为 0,即 corePool 为空。maximumPoolSize 被设置为 Integer.MAX_VALUE,即 maximumPool 是无界的。这里把 keepAliveTime 设置为 60L,意味着 CachedThreadPool 中的空闲线程等待新任务的最长时间为 60 秒,空闲线程超过 60 秒后将会被终止

CachedThreadPool 使用没有容量的 SynchronousQueue 作为线程池的工作队列,但 CachedThreadPool 的 maximumPool 是无界的。这意味着,如果主线程提交任务的速度高于 maximumPool 中线程处理任务的速度,CachedThreadPool 会不断创建新线程。极端情况下,CachedThreadPool 会因为创建过多线程而耗尽 CPU 和内存资源


ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor 继承 ThreadPoolExecutor 并实现 ScheduledExecutorService 接口,用于在给定的延迟后执行任务或者定期执行任务

可以使用 Executors 工具类创建两种 ScheduledThreadPoolExecutor:

// 适用于若干个(固定)线程延时或者定期执行任务,同时为了满足资源管理的需求而需要限制后台线程数量的场景
ScheduledExecutorService stp = Executors.newScheduledThreadPool(int threadNums);
ScheduledExecutorService stp = Executors.newScheduledThreadPool(int threadNums, ThreadFactory threadFactory);
// 适用于需要单个线程延时或者定期的执行任务,同时需要保证各个任务顺序执行的应用场景
ScheduledExecutorService stse = Executors.newSingleThreadScheduledExecutor(int threadNums);
ScheduledExecutorService stp = Executors.newSingleThreadScheduledExecutor(int threadNums, ThreadFactory threadFactory);

ScheduledThreadPoolExecutor 会把任务封装为 ScheduledFutureTask,放到一个延迟队列(DelayQueue)中,ScheduledFutureTask 主要包含三个成员变量

  • long 型成员变量 time,表示这个任务将要被执行的具体时间
  • long 型成员变量 sequenceNumber,表示这个任务被添加到 ScheduledThreadPoolExecutor 中的序号
  • long 型成员变量 period,表示任务执行的间隔周期

DelayQueue 封装了一个 PriorityQueue,这个 PriorityQueue 会对队列中的 ScheduledFutureTask 进行排序。排序时,time 小的排在前面(时间早的任务将被先执行)。如果两个 ScheduledFutureTask 的 time 相同,就比较 sequenceNumber,sequenceNumber 小的排在前面(如果两个任务的执行时间相同,先提交的任务先执行)

下图是 ScheduledThreadPoolExecutor 中的线程执行周期任务的过程

  • 线程 1 从 DelayQueue 获取已到期的 ScheduledFutureTask,到期任务是指 ScheduledFutureTask 的 time 小于等于当前时间
  • 线程 1 执行这个 ScheduledFutureTask
  • 线程 1 修改 ScheduledFutureTask 的 time 变量为下次将要被执行的时间
  • 线程 1 把修改 time 之后的 ScheduledFutureTask 放回 DelayQueue 中

FutureTask

Future 接口和实现 Future 接口的 FutureTask 类,代表异步计算的结果。FutureTask 除了实现 Future 接口外,还实现了 Runnable 接口。因此,FutureTask 可以交给 Executor 执行,也可以由调用线程直接执行 FutureTask.run()。根据 FutureTask.run() 方法被执行的时机,FutureTask可以处于下面三种状态:

  • 未启动

    FutureTask.run() 方法还没有被执行之前,FutureTask 处于未启动状态,当创建一个 FutureTask,且没有执行 FutureTask.run() 方法之前,这个 FutureTask 处于未启动状态

  • 已启动

    FutureTask.run() 方法被执行的过程中,FutureTask 处于已启动状态

  • 已完成

FutureTask.run() 方法执行完后正常结束,或被取消 FutureTask.cancel(…),或执行 FutureTask.run() 方法时抛出异常而结束,FutureTask 处于已完成状态

可以把 FutureTask 交给 Executor 执行,也可以通过 ExecutorService.submit(...) 方法返回一个 FutureTask,然后执行 FutureTask.get() 方法获取结果或 FutureTask.cancel(...) 方法取消


posted @ 2021-04-04 09:48  低吟不作语  阅读(481)  评论(0编辑  收藏  举报