06_任务的执行
任务的执行
一、在线程中执行任务
1.1 串行执行
串行执行,是指所有任务都在单个线程中串行地执行。
1 | class SingleThreadWebServer { |
串行执行,一般来说吞吐率或速响应性都比较差。
1.2 独立线程执行
独立线程执行,是指为每个任务单独创建一个新线程,并在新线程中执行任务。
1 | class ThreadPerTaskWebServer { |
这种方式,可以提供高吞吐率和快响应性。
不过为每个任务创建独立的线程,这受限于服务器的负载能力。
1.3 无限制创建线程存在的问题
- 线程生命周期的开销非常高:线程的创建和销毁是有代价的,会消耗大量的计算资源
- 资源消耗:活跃的线程会消耗系统资源,特别是内存资源
- 稳定性:系统中可创建的线程数量是有限制的,受多个因素制约
二、在线程池中执行任务
在前面的任务执行方式中:
- 串行执行,吞吐率和响应性太差
- 为每个任务创建独立线程运行,资源管理太麻烦
为此,可以取一种折中的方案:
- 创建包含 n 个线程的线程池,来执行 m 个任务,线程和任务比例是 n:m
- 要求线程池的线程是可以复用的,因为它需要执行多个任务
这种线程池方式,既避免了串行执行,也避免了资源不足的情况。
不过,实际的线程池会更复杂,它的基本设计理念是:
- 将任务的提交过程与执行过程解耦开来
- 用 Runnable 表示任务
- 用 Executor 来提供任务执行的平台
- Executor 还提供了对生命周期的支持、信息统计、程序管理、性能监控等机制
线程池的使用如下:
1 | class TaskExecutorWebServer { |
实际上,线程池就是提供了自动管理线程和任务的功能。
2.1 执行策略
线程池中,还提供了设置执行策略的方式。
执行策略定义了任务执行的各种情况:
- 在什么线程中执行任务?
- 任务按照什么顺序执行(FIFO、LIFO、优先级)?
- 有多少个任务可以并发执行?
- 任务等待队列能放多少个?
- 任务拒绝策略是什么(丢弃最新任务、丢弃最老任务)?
- 任务执行前后,应该执行哪些动作?
在创建线程池时,可以自行决定选择何种执行策略,以适应实际的需求场景。
2.2 线程池类型
线程池的优点:
- 自动管理线程和任务
- 可以重用线程,避免线程创建和销毁带来的巨大开销
- 线程不销毁,可以快速执行任务,从而避免了任务的延迟执行,提高了响应性
- 防止创建线程过多,降低了线程之间的竞争,避免系统资源耗尽
Java 类库中,提供了几种常用的线程池:
- 固定长度线程池(newFixedThreadPool):限制了线程最大数量,每次提交任务时,就会创建一个线程,直到达到线程最大数量为止
- 可缓存线程池(newCachedThreadPool):每次提交任务,就会创建一个新线程,不存在线程数量限制
- 单线程池(newSingleThreadExecutor):线程池中只有一个线程,提交任务时相当于串行执行
- 定时线程池(newScheduledThreadPool):限制了线程最大数量,以延迟或定时方式执行任务
除了这几种常用线程池,Java 类库中还提供了自定义线程池,不过需要自己实现接口。
2.3 线程池生命周期
JVM 只有在所有(非守护)线程全部终止后才会退出,所以线程池的线程也要能结束才行。
ExecutorService 扩展了 Executor 接口,提供了线程池的生命周期。
线程池的生命周期有3种状态:
- 运行:线程池创建后,就处于运行状态
- 关闭:执行
shutdown
/shotdownNow
方法后,线程池处于关闭状态,拒绝提交新任务,但是会等已提交任务执行结束才终止 - 终止:线程池中所有任务都完成后,进入终止状态
比如,支持关闭操作的线程池:
1 | public class LifecycleWebServer { |
程序结束时,必须把线程池关闭,否则线程是不会被回收的,JVM 也不会终止。
三、任务的执行
3.1 找出任务的并行性
找到任务的并行性,提取出可并发执行的任务:
- 有时候,任务边界是不明显的,需要找出可并发执行的任务
- 使用线程池执行任务,要求必须将任务描述为一个 Runnable
比如说,渲染 HTML 页面,渲染图片和渲染其他节点可以分开成不同的任务并发执行。
3.2 确定任务的类型
在执行任务前,应该要确认任务的类型,它需要怎么执行:
- Runnable 是普通的可执行的任务,没有返回值
- Callable 是带有返回值的的任务
- TimerTask 是定时任务,可以延迟或定时执行
- Future 提供了有用的接口,可以取消任务的执行
- …
不同类型的任务,执行的方式不同,找出最合适的任务类型来执行。
3.3 提高任务的并发性
- 只有大量相互独立且同构的任务并发执行,才能带来真正的性能提升
- 尽量提交相互独立的任务,减少依赖的任务
- 尽量把不同任务类型,提交到不同类型的线程池执行
3.4 等待任务的完成
有时候需要等待任务完成后,再执行某些操作,有多种方式可以等待:
- 保留与每个任务关联的
Future
对象,使用它等待任务完成 - 使用
CompletionService
提供的方法,等待任务完成 - 使用
ExecutorService
的invokeAll
方法,等待所有任务完成
根据实际需求,是等待单个任务完成,还是等待所有任务完成,再选择不同的方式。
3.5 为任务设置时限
有时候,任务无法在指定时间内完成,那么就不再需要它的结果了,可以放弃这个任务。
- 可以为任务设置执行时限,超过时限后,任务会被取消,并且不再等待它完成
- 任务超时后,应该立即停止,避免继续计算浪费计算资源
- 需要评估任务的执行时间,才能确定任务的执行时限
不是所有任务都需要时限,视具体情况而定。
总结
在线程中执行任务:
- 串行执行:所有任务都在一个线程内执行,吞吐率和响应性差
- 独立线程执行:为每个任务创建独立线程执行,资源消耗过大,且不稳定
在线程池中运行:
- 创建包含 n 个线程的线程池,来执行 m 个任务,线程和任务比例是 n:m
- 要求线程池的线程是可以复用的,因为它需要执行多个任务
- 将任务的提交过程与执行过程解耦开来
- 用 Runnable 表示任务
- 用 Executor 来提供任务执行的平台
- Executor 还提供了对生命周期的支持、信息统计、程序管理、性能监控等机制
执行策略:
- 在什么线程中执行任务?
- 任务按照什么顺序执行(FIFO、LIFO、优先级)?
- 有多少个任务可以并发执行?
- 任务等待队列能放多少个?
- 任务拒绝策略是什么(丢弃最新任务、丢弃最老任务)?
- 任务执行前后,应该执行哪些动作?
线程池优点:
- 自动管理线程和任务
- 可以重用线程,避免线程创建和销毁带来的巨大开销
- 线程不销毁,可以快速执行任务,从而避免了任务的延迟执行,提高了响应性
- 防止创建线程过多,降低了线程之间的竞争,避免系统资源耗尽
常见线程池:
- 固定长度线程池(newFixedThreadPool):限制了线程最大数量,每次提交任务时,就会创建一个线程,直到达到线程最大数量为止
- 可缓存线程池(newCachedThreadPool):每次提交任务,就会创建一个新线程,不存在线程数量限制
- 单线程池(newSingleThreadExecutor):线程池中只有一个线程,提交任务时相当于串行执行
- 定时线程池(newScheduledThreadPool):限制了线程最大数量,以延迟或定时方式执行任务
线程池生命周期:
- 运行:线程池创建后,就处于运行状态
- 关闭:执行
shutdown
/shotdownNow
方法后,线程池处于关闭状态,拒绝提交新任务,但是会等已提交任务执行结束才终止 - 终止:线程池中所有任务都完成后,进入终止状态
任务的执行:
- 找出任务的并行性
- 确定任务的类型
- 提高任务的并发性
- 等待任务完成
- 为任务设置时限