这是我参与8月更文挑战的第3天,活动详情查看:8月更文挑战
0. 前言
在 Java 中,可以使用线程来异步执行任务,线程创建和销毁时需要一定的开销,如果执行每个任务的时候都创建新的线程,那么系统可能因处于高负荷的状态导致崩溃
Java 的线程既是工作单元,也是执行机制。工作单元包括 Runnable 和 Callable,执行机制由 Executor 框架提供
1. Executor 框架简介
1.1 Executor 框架的两级调度模型
- 在上层,Java 多线程程序把应用分解为若干个任务,然后使用用户级的调度器(Executor 框架)将任务映射到固定数量的线程上
- 在下层,操作系统内核将这些线程映射到硬件处理器(CPU)上
应用程序通过 Executor 框架控制上层的调度,而下层的调度由操作系统内核控制,下层的调度不受应用程序的控制
1.2 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 框架的使用
- 主线程创建实现 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):返回结果为 nullcallable(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() 方法:
- 如果工作线程数少于 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() 方法:
- 线程池中没有线程时,创建一个线程来执行任务
- 线程池中有一个线程时,将任务添加进 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() 方法:
- 首先执行
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 的区别
| Timer | ScheduledThreadPoolExecutor |
|---|---|
| 无法捕获任务执行中的异常,如果出现一次异常则整个定时任务停止 | 可以捕获异常 |
| 基于 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 时间后执行下一个任务,周期性执行 -
scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit):延迟 delay 时间后执行任务。在上一个任务执行结束时计算,延迟 period 时间后执行下一个任务,周期性执行
3.3 ScheduledThreadPoolExecutor 的任务传递
- 当调用发布任务的方法时,会将任务添加进 DelayedWorkQueue 任务队列
- 线程池中的工作线程会循环从 DelayedWorkQueue 任务队列获取 RunnableScheduledFuture 任务执行
3.4 DelayedWorkQueue 任务队列
- 一个优先级的无界阻塞队列,每次取出的任务是当前队列中目标执行时间最靠前的
- 线程池中的工作线程获取任务时,只有等到任务达到目标执行时间后才能成功获取任务并执行
- 保存任务的队列结构是由最小堆算法实现的数组,保证了第一个元素是目标执行时间最靠前的任务,插入和取出元素操作的最坏时间复杂度为 O(logN)
3.5 ScheduledFutureTask 任务
主要属性:
- sequenceNumber:任务的序号
- time:目标执行时间
- period:执行的间隔时间
任务在任务队列中的排序:
- time 小的靠前
- time 相同时,sequenceNumber 小的靠前
任务执行步骤:
- 线程池中的线程从任务队列中获取任务
- 如果任务的目标执行时间已到达,则线程执行任务
- 线程执行完任务后,根据任务的执行的间隔时间 period 修改目标执行时间 time
- 线程修改 time 后,将任务重新添加到队列中
4. FutureTask 详解
Future 接口及其实现类 FutureTask 类代表异步计算的结果
4.1 三种状态
- 未启动:当创建一个 FutureTask,且未调用
run()方法,这时处于未启动状态 - 已启动:调用
run()方法,处于已启动状态 - 已完成:
run()方法执行完后,处于已完成状态
4.2 核心方法
get() 方法:
get()方法:调用线程一直阻塞着等待任务执行完成获取结果get(long timeout, TimeUnit unit)方法:调用线程阻塞着等待获取结果,当超过指定时间后任务还未执行完成,则抛出异常
cancel() 方法:
- 当任务未启动,调用
cancel(...)方法后,任务永远不会被执行 - 当任务已启动,调用
cancel(true)方法会中断执行任务的线程来试图停止任务,调用cancel(false)方法不会对执行中的任务产生影响 - 当线程已完成,调用
cancel(...)方法后返回 false