06_任务的执行

任务的执行

一、在线程中执行任务

1.1 串行执行

串行执行,是指所有任务都在单个线程中串行地执行。

1
2
3
4
5
6
7
8
9
class SingleThreadWebServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(80);
while (true) {
Socket connection = serverSocket.accept();
handleRequest(connection);
}
}
}

串行执行,一般来说吞吐率或速响应性都比较差。

1.2 独立线程执行

独立线程执行,是指为每个任务单独创建一个新线程,并在新线程中执行任务。

1
2
3
4
5
6
7
8
9
class ThreadPerTaskWebServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(80);
while (true) {
Socket connection = serverSocket.accept();
new Thread(() -> handleRequest(connection)).start();
}
}
}

这种方式,可以提供高吞吐率和快响应性。

不过为每个任务创建独立的线程,这受限于服务器的负载能力。

1.3 无限制创建线程存在的问题

  • 线程生命周期的开销非常高:线程的创建和销毁是有代价的,会消耗大量的计算资源
  • 资源消耗:活跃的线程会消耗系统资源,特别是内存资源
  • 稳定性:系统中可创建的线程数量是有限制的,受多个因素制约

二、在线程池中执行任务

在前面的任务执行方式中:

  • 串行执行,吞吐率和响应性太差
  • 为每个任务创建独立线程运行,资源管理太麻烦

为此,可以取一种折中的方案:

  • 创建包含 n 个线程的线程池,来执行 m 个任务,线程和任务比例是 n:m
  • 要求线程池的线程是可以复用的,因为它需要执行多个任务

这种线程池方式,既避免了串行执行,也避免了资源不足的情况。

不过,实际的线程池会更复杂,它的基本设计理念是:

  • 将任务的提交过程与执行过程解耦开来
  • 用 Runnable 表示任务
  • 用 Executor 来提供任务执行的平台
  • Executor 还提供了对生命周期的支持、信息统计、程序管理、性能监控等机制

线程池的使用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
class TaskExecutorWebServer {

static final int NUM_THREADS = 10;
static final ExecutorService executor = Executors.newFixedThreadPool(NUM_THREADS);

public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(80);
while (true) {
Socket connection = serverSocket.accept();
executor.execute(() -> handleRequest(connection));
}
}
}

实际上,线程池就是提供了自动管理线程和任务的功能。

2.1 执行策略

线程池中,还提供了设置执行策略的方式。

执行策略定义了任务执行的各种情况:

  • 在什么线程中执行任务?
  • 任务按照什么顺序执行(FIFO、LIFO、优先级)?
  • 有多少个任务可以并发执行?
  • 任务等待队列能放多少个?
  • 任务拒绝策略是什么(丢弃最新任务、丢弃最老任务)?
  • 任务执行前后,应该执行哪些动作?

在创建线程池时,可以自行决定选择何种执行策略,以适应实际的需求场景。

2.2 线程池类型

线程池的优点:

  • 自动管理线程和任务
  • 可以重用线程,避免线程创建和销毁带来的巨大开销
  • 线程不销毁,可以快速执行任务,从而避免了任务的延迟执行,提高了响应性
  • 防止创建线程过多,降低了线程之间的竞争,避免系统资源耗尽

Java 类库中,提供了几种常用的线程池:

  • 固定长度线程池(newFixedThreadPool):限制了线程最大数量,每次提交任务时,就会创建一个线程,直到达到线程最大数量为止
  • 可缓存线程池(newCachedThreadPool):每次提交任务,就会创建一个新线程,不存在线程数量限制
  • 单线程池(newSingleThreadExecutor):线程池中只有一个线程,提交任务时相当于串行执行
  • 定时线程池(newScheduledThreadPool):限制了线程最大数量,以延迟或定时方式执行任务

除了这几种常用线程池,Java 类库中还提供了自定义线程池,不过需要自己实现接口。

2.3 线程池生命周期

JVM 只有在所有(非守护)线程全部终止后才会退出,所以线程池的线程也要能结束才行。

ExecutorService 扩展了 Executor 接口,提供了线程池的生命周期。

线程池的生命周期有3种状态:

  • 运行:线程池创建后,就处于运行状态
  • 关闭:执行 shutdown/shotdownNow 方法后,线程池处于关闭状态,拒绝提交新任务,但是会等已提交任务执行结束才终止
  • 终止:线程池中所有任务都完成后,进入终止状态

比如,支持关闭操作的线程池:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class LifecycleWebServer {

private final int NUM_THREADS = 10;
private final ExecutorService executor = Executors.newFixedThreadPool(NUM_THREADS);

public void start() throws IOException {
ServerSocket serverSocket = new ServerSocket(80);
while (!executor.isShutdown()) {
try {
Socket connection = serverSocket.accept();
executor.execute(() -> handleRequest(connection));
} catch (RejectedExecutionException e) {
if (!executor.isShutdown()) {
e.printStackTrace();
}
}
}
}

public void stop() {
executor.shutdown();
}

}

程序结束时,必须把线程池关闭,否则线程是不会被回收的,JVM 也不会终止。

三、任务的执行

3.1 找出任务的并行性

找到任务的并行性,提取出可并发执行的任务:

  • 有时候,任务边界是不明显的,需要找出可并发执行的任务
  • 使用线程池执行任务,要求必须将任务描述为一个 Runnable

比如说,渲染 HTML 页面,渲染图片和渲染其他节点可以分开成不同的任务并发执行。

3.2 确定任务的类型

在执行任务前,应该要确认任务的类型,它需要怎么执行:

  • Runnable 是普通的可执行的任务,没有返回值
  • Callable 是带有返回值的的任务
  • TimerTask 是定时任务,可以延迟或定时执行
  • Future 提供了有用的接口,可以取消任务的执行

不同类型的任务,执行的方式不同,找出最合适的任务类型来执行。

3.3 提高任务的并发性

  • 只有大量相互独立且同构的任务并发执行,才能带来真正的性能提升
  • 尽量提交相互独立的任务,减少依赖的任务
  • 尽量把不同任务类型,提交到不同类型的线程池执行

3.4 等待任务的完成

有时候需要等待任务完成后,再执行某些操作,有多种方式可以等待:

  • 保留与每个任务关联的 Future 对象,使用它等待任务完成
  • 使用 CompletionService 提供的方法,等待任务完成
  • 使用 ExecutorServiceinvokeAll 方法,等待所有任务完成

根据实际需求,是等待单个任务完成,还是等待所有任务完成,再选择不同的方式。

3.5 为任务设置时限

有时候,任务无法在指定时间内完成,那么就不再需要它的结果了,可以放弃这个任务。

  • 可以为任务设置执行时限,超过时限后,任务会被取消,并且不再等待它完成
  • 任务超时后,应该立即停止,避免继续计算浪费计算资源
  • 需要评估任务的执行时间,才能确定任务的执行时限

不是所有任务都需要时限,视具体情况而定。

总结

在线程中执行任务:

  • 串行执行:所有任务都在一个线程内执行,吞吐率和响应性差
  • 独立线程执行:为每个任务创建独立线程执行,资源消耗过大,且不稳定

在线程池中运行:

  • 创建包含 n 个线程的线程池,来执行 m 个任务,线程和任务比例是 n:m
  • 要求线程池的线程是可以复用的,因为它需要执行多个任务
  • 将任务的提交过程与执行过程解耦开来
  • 用 Runnable 表示任务
  • 用 Executor 来提供任务执行的平台
  • Executor 还提供了对生命周期的支持、信息统计、程序管理、性能监控等机制

执行策略:

  • 在什么线程中执行任务?
  • 任务按照什么顺序执行(FIFO、LIFO、优先级)?
  • 有多少个任务可以并发执行?
  • 任务等待队列能放多少个?
  • 任务拒绝策略是什么(丢弃最新任务、丢弃最老任务)?
  • 任务执行前后,应该执行哪些动作?

线程池优点:

  • 自动管理线程和任务
  • 可以重用线程,避免线程创建和销毁带来的巨大开销
  • 线程不销毁,可以快速执行任务,从而避免了任务的延迟执行,提高了响应性
  • 防止创建线程过多,降低了线程之间的竞争,避免系统资源耗尽

常见线程池:

  • 固定长度线程池(newFixedThreadPool):限制了线程最大数量,每次提交任务时,就会创建一个线程,直到达到线程最大数量为止
  • 可缓存线程池(newCachedThreadPool):每次提交任务,就会创建一个新线程,不存在线程数量限制
  • 单线程池(newSingleThreadExecutor):线程池中只有一个线程,提交任务时相当于串行执行
  • 定时线程池(newScheduledThreadPool):限制了线程最大数量,以延迟或定时方式执行任务

线程池生命周期:

  • 运行:线程池创建后,就处于运行状态
  • 关闭:执行 shutdown/shotdownNow 方法后,线程池处于关闭状态,拒绝提交新任务,但是会等已提交任务执行结束才终止
  • 终止:线程池中所有任务都完成后,进入终止状态

任务的执行:

  • 找出任务的并行性
  • 确定任务的类型
  • 提高任务的并发性
  • 等待任务完成
  • 为任务设置时限
作者

jiaduo

发布于

2022-05-15

更新于

2023-04-03

许可协议