Java并发编程初识-Executor框架| 8月更文挑战

174 阅读9分钟

这是我参与8月更文挑战的第3天,活动详情查看:8月更文挑战

0. 前言

在 Java 中,可以使用线程来异步执行任务,线程创建和销毁时需要一定的开销,如果执行每个任务的时候都创建新的线程,那么系统可能因处于高负荷的状态导致崩溃

Java 的线程既是工作单元,也是执行机制。工作单元包括 Runnable 和 Callable,执行机制由 Executor 框架提供

1. Executor 框架简介

1.1 Executor 框架的两级调度模型

任务的两级调度模型
  • 在上层,Java 多线程程序把应用分解为若干个任务,然后使用用户级的调度器(Executor 框架)将任务映射到固定数量的线程上
  • 在下层,操作系统内核将这些线程映射到硬件处理器(CPU)上

应用程序通过 Executor 框架控制上层的调度,而下层的调度由操作系统内核控制,下层的调度不受应用程序的控制

1.2 Executor 框架的结构

Executor框架的类与接口
1.2.1 Executor 框架的3大组成部分
  • 任务:被执行任务需要实现接口 Runnable 或 Callable
  • 任务的执行:核心接口 Executor,以及继承 Executor 的 ExecutorService 接口。ExecutorService 接口的关键实现类:ThreadPoolExecutor、ScheduledThreadPoolExecutor
  • 异步计算的结果:接口 Future 和实现类 FutureTask
1.2.2 类和接口的简介
  • Executor 接口:Executor 框架的基础,将任务的提交和执行分离开来
  • ThreadPoolExecutor 类:线程池的核心实现类,用来执行被提交的任务
  • ScheduledThreadPoolExecutor 类:在指定延迟后执行命令,或定期执行命令。比 Timer 更灵活强大
  • Future 接口、FutureTask 实现类:异步计算的结果
  • Runnable 、Callable 接口:实现该接口的任务都可以被 ThreadPoolExecutor 和 ScheduledThreadPoolExecutor 执行
1.2.3 Executor 框架的使用
Executor框架的使用示意图
  • 主线程创建实现 Runnable 或 Callable 接口的任务对象。Executors 工具类可以把 Runnable 对象封装为 Callable 对象:
    • Executors.callable(Runnable task)
    • Executors.callable(Runnable task, T result)
  • ExecutorService 执行 Runnable 或 Callable 任务对象:
    • ExecutorService.execute(Runnable command)
    • ExecutorService.submit(Runnable task)
    • ExecutorService.submit(Callable<T> task)
  • 如果执行 ExecutorService.submit(...) ,ExecutorService 将返回实现 Future 接口的 FutureTask 对象。也可以将 FutureTask 直接交给 ExecutorService 执行
  • 主线程可以执行 FutureTask.get() 来等待任务执行完成,也可以执行 FutureTask.cancel(boolean mayInterruptIfRunning) 取消任务执行

1.3 Executor 框架的成员

1.3.1 ThreadPoolExecutor

通常使用 Executors 工具类创建,可创建3种类型:

  • FixedThreadPool:固定线程数的线程池,适用于因需要资源管理而限制线程数量的场景,适用于负载较重的服务器
  • SingleThreadExecutor:单个线程的线程池,适用于需要保证顺序执行任务,并且在任意时间点不会有多个线程活动的场景
  • CachedThreadPool:大小无界的线程池,适用于执行大量短期异步任务的小程序,或负载较轻的服务器
1.3.2 ScheduledThreadPoolExecutor

通常使用 Executors 工具类创建,可创建2种类型:

  • ScheduledThreadPool:包含若干线程,适用于多个后台线程执行周期任务,同时满足资源管理需求而限制线程数的场景
  • SingleThreadScheduledExecutor:包含单个线程,适用于单个后台线程执行周期任务,同时保证了任务执行的顺序的场景
1.3.3 Future 接口

Future 接口、FutureTask 实现类表示了异步计算的结果。当使用 submit(...) 提交任务给线程池时,线程池会返回 FutureTask 对象

  • Future<T> submit(Callable<T> task)
  • Future<?> submit(Runnable task)
  • Future<T> submit(Runnable task, T result)
1.3.4 Runnable 和 Callable 接口

实现下述接口的任务对象对可以被线程池执行

  • Runnable:不返回结果
  • Callable:返回结果

Executors 工具类还提供了把 Runnable 包装成 Callable 的方法:

  • callable(Runnable task):返回结果为 null
  • callable(Runnable task, T result):返回结果为 result

2. ThreadPoolExecutor 详解

Executor 框架最核心的类 ThreadPoolExecutor,是线程池的实现类,主要属性:

  • corePoolSize:核心线程数
  • maximumPoolSize:最大线程数
  • BlockingQueue:任务队列
  • RejectedExecutionHandler:拒绝策略处理器,当线程池的线程数已达最大值且任务队列已满时,需要执行拒绝方法处理任务

Executors 工具类创建的3种线程池:

  • FixedThreadPool
  • SingleThreadExecutor
  • CachedThreadPool

2.1 FixedThreadPool 详解

public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>(),
                                  threadFactory);
}
  • corePoolSize 和 maximumPoolSize 都设置为 nThreads
  • 当线程池工作线程数大于 corePoolSize 时,keepAliveTime 为空闲线程等待获取任务的时间,超过该时间线程将被销毁。这里 keepAliveTime 为 0L 表示空闲线程立即销毁
FixedThreadPool的execute()的运行示意图

FixedThreadPool 执行 execute() 方法:

  • 如果工作线程数少于 corePoolSize,则创建线程来执行任务
  • 如果工作线程数等于 corePoolSize,将任务添加进 LinkedBlockingQueue
  • 工作线程执行完步骤1后,循环从 LinkedBlockingQueue 获取任务执行

FixedThreadPool 使用 LinkedBlockingQueue 无界队列(容量为 Integer.MAX_VALUE)作为任务队列,将会带来下列影响:

  • 当工作线程数等于 corePoolSize 时,提交的任务都将添加到任务队列中,线程池的工作线程数不会超过 corePoolSize
  • maximumPoolSize 参数无效
  • keepAliveTime 参数无效
  • 线程池(正常运行状态)不会拒绝任务而调用 RejectedExecutionHandler 的拒绝方法

2.2 SingleThreadExecutor 详解

public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>(),
                                threadFactory));
}
  • corePoolSize 和 maximumPoolSize 都设置为1,单个线程执行任务
  • 其他与 FixedThreadPool 类似
SingleThreadExecutor的execute()的运行示意图

SingleThreadExecutor 执行 execute() 方法:

  • 线程池中没有线程时,创建一个线程来执行任务
  • 线程池中有一个线程时,将任务添加进 LinkedBlockingQueue
  • 线程执行完步骤1的任务时,循环从 LinkedBlockingQueue 获取任务执行

2.3 CachedThreadPool 详解

CachedThreadPool 是个根据需要创建线程的线程池

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}
  • corePoolSize 设置为0,maximumPoolSize 设置为 Integer.MAX_VALUE,表示线程池无需核心线程(一般情况下核心线程不会超时销毁),并且可以无限创建线程
  • keepAliveTime 设置为60L,表示工作线程空闲(任务队列没有任务获取时的等待状态)超过60s后销毁

CachedThreadPool 使用 SynchronousQueue 无容量阻塞队列作为任务队列,意味每次提交任务到线程池,必须对应着一次工作线程获取任务。当主线程提交任务的速度快过工作线程执行任务时,线程池将无限创建线程,最坏的情况下创建过多线程而耗尽 CPU 和内存资源

CachedThreadPool的execute()的运行示意图

CachedThreadPool 执行 execute() 方法:

  • 首先执行 SynchronousQueue.offer(Runnable task) 方法,如果线程池有空闲的工作线程正在执行 SynchronousQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) 方法等待获取任务,那么主线程将任务交给工作线程执行,否则执行步骤2
  • 如果线程池为空或没有空闲的工作线程,则新增线程来执行任务
  • 步骤2的工作线程执行完任务后,将调用 SynchronousQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) 方法获取任务,该方法最多让线程等待60s,如果超过60s后还没任务可以执行,则线程销毁

3. ScheduledThreadPoolExecutor 详解

ScheduledThreadPoolExecutor 继承 ThreadPoolExecutor,主要用来执行延迟一定时间后执行的任务,或者周期执行的任务

3.1 ScheduledThreadPoolExecutor 和 Timer 的区别

TimerScheduledThreadPoolExecutor
无法捕获任务执行中的异常,如果出现一次异常则整个定时任务停止可以捕获异常
基于 Date 时钟,分布式环境下由于服务器时钟不一致可能出现问题基于相对时间
只会创建一个线程,任务串行执行,前一个任务执行超时会影响到下一个任务核心原理是线程池中的线程并发执行任务

3.2 发布任务的核心方法

  • schedule(Runnable command, long delay, TimeUnit unit) :延迟 delay 时间后执行任务,获取执行结果为空

  • schedule(Callable<V> callable, long delay, TimeUnit unit) :延迟 delay 时间后执行任务,获取执行结果不为空

  • scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) :延迟 delay 时间后执行任务。在上一个任务执行开始时计算,延迟 period 时间后执行下一个任务,周期性执行

    scheduleAtFixedRate

  • scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) :延迟 delay 时间后执行任务。在上一个任务执行结束时计算,延迟 period 时间后执行下一个任务,周期性执行

    scheduleWithFixedDelay

3.3 ScheduledThreadPoolExecutor 的任务传递

ScheduledThreadPoolExecutor的任务传递示意图

  • 当调用发布任务的方法时,会将任务添加进 DelayedWorkQueue 任务队列
  • 线程池中的工作线程会循环从 DelayedWorkQueue 任务队列获取 RunnableScheduledFuture 任务执行

3.4 DelayedWorkQueue 任务队列

  • 一个优先级的无界阻塞队列,每次取出的任务是当前队列中目标执行时间最靠前的
  • 线程池中的工作线程获取任务时,只有等到任务达到目标执行时间后才能成功获取任务并执行
  • 保存任务的队列结构是由最小堆算法实现的数组,保证了第一个元素是目标执行时间最靠前的任务,插入和取出元素操作的最坏时间复杂度为 O(logN)

3.5 ScheduledFutureTask 任务

主要属性:

  • sequenceNumber:任务的序号
  • time:目标执行时间
  • period:执行的间隔时间

任务在任务队列中的排序:

  • time 小的靠前
  • time 相同时,sequenceNumber 小的靠前

任务执行步骤:

ScheduledFutureTask任务的执行步骤

  • 线程池中的线程从任务队列中获取任务
  • 如果任务的目标执行时间已到达,则线程执行任务
  • 线程执行完任务后,根据任务的执行的间隔时间 period 修改目标执行时间 time
  • 线程修改 time 后,将任务重新添加到队列中

4. FutureTask 详解

Future 接口及其实现类 FutureTask 类代表异步计算的结果

4.1 三种状态

FutureTask的状态迁移图

  • 未启动:当创建一个 FutureTask,且未调用 run() 方法,这时处于未启动状态
  • 已启动:调用 run() 方法,处于已启动状态
  • 已完成:run() 方法执行完后,处于已完成状态

4.2 核心方法

FutureTask核心方法执行图

get() 方法:

  • get() 方法:调用线程一直阻塞着等待任务执行完成获取结果
  • get(long timeout, TimeUnit unit) 方法:调用线程阻塞着等待获取结果,当超过指定时间后任务还未执行完成,则抛出异常

cancel() 方法:

  • 当任务未启动,调用 cancel(...) 方法后,任务永远不会被执行
  • 当任务已启动,调用 cancel(true) 方法会中断执行任务的线程来试图停止任务,调用 cancel(false) 方法不会对执行中的任务产生影响
  • 当线程已完成,调用 cancel(...) 方法后返回 false