Open funnycoding opened 4 years ago
本章是《Java 并发编程实战》 第二部分 —— 「结构化并发应用」的第1章 —— 任务执行。 大概浏览下来是围绕着 「线程池」,「Future」,「FutureTask」,线程池的生命周期,线程池中任务的取消以及线程池关闭时发生的一系列动作来讲的。有一个比较连续的通过引入线程池到一步步优化改造的例子。
本章是《Java 并发编程实战》 第二部分 —— 「结构化并发应用」的第1章 —— 任务执行。 大概浏览下来是围绕着 「线程池」,「Future」,「FutureTask」,线程池的生命周期,线程池中任务的取消以及线程池关闭时发生的一系列动作来讲的。有一个比较连续的通过引入线程池到一步步优化改造的例子。
本章是总体来说是一个偏应用的章节,应该说从这章开始都是偏应用的了,该将的基础前5章已经讲完了,所以知识的密度一下子下降了很多,是很容易上手的章节。 大多数并发应用程序都是围绕 **「任务执行(Task Execution)」**来进行构造的,「任务」 通常是一些抽象的且离散的**「工作单元」**。 通过把应用程序的工作分解到多个**「任务」**中,可以简化程序的组织结构,提供一种**「自然的事务边界」**来优化错误的恢复过程,以及提供一种自然的**「并行工作结构」**来提升并发性。 第三节有一个非常精彩的例子,一步步的演化,改进,拓展了视野,学到了一些场景下的最佳实践。(当然这本书有些年头了,所以这些最佳实践有些过时,不过总比之前什么都不知道要强多了。) 这章总的来说,知识密度不高,但是很能拓宽你的视野,如果你之前是对并发没有什么实际的具体应用的选手的话,那对你的帮助还是挺大的。 ### 6.1 在线程中执行任务 当围绕 **「任务执行」** 来设计应用程序结构时,第一部就是要找出**清晰**的**「任务边界」**。在理想的情况下,各个任务之间是独立的:任务并不依赖于其他任务的状态,结果或边界效应。 **「独立性」**有助于实现并发,如果存在足够多的**「资源」**,那么这些独立的任务都可以并行执行。为了在**「调度」**与**「负载均衡」**等过程中实现更高的**「灵活性」**,每项任务还表示应用程序的一小部分处理能力。 在**「正常的负载」**下,服务器应用程序应该同时表现出良好**「吞吐量」** 和 快速的 **「响应性」**。 应用程序提供商希望支持尽可能多的用户,而用户希望得到尽可能快的响应。当「负荷过载」时,应用程序的性能应该是逐渐降低,而不是直接失败。 要实现上述目标,应该选择清晰的**「任务边界」**以及明确的任务执行策略([参见6.2.2 节]()。) 大多数服务器应用程序都提供了一种自然的任务边界选择方式:以「独立的客户请求」为边界。 Web 服务器,邮件服务器,文件服务器,EJB 容器以及数据库服务器等,这些服务器都通过网络接受远程客户的连接请求。将**「独立的请求」**作为**「任务边界」**,既可以实现「任务的独立性」,又可以实现合理的「任务规模」。 例如:在向邮件服务器提交一个消息后得到的结果,并不受其他正在处理的消息的影响,而且在处理单个消息时通常只需要服务器总处理能力的很小一部分。 #### 6.1.1 串行地执行任务 在应用程序中可以通过多种策略来调度任务,而其中一些策略能够更好地利用潜在的并发性。最简单的策略就是在单个线程中**「串行」**地执行各项任务。 **<---【也就是不使用多线程技术,所有业务逻辑都在一个线程中完成】** **程序清单 6-1** 中的 `SingleThreadWebServer` 将串行地处理它的任务(通过 80 端口接收到的 HTTP 请求)。 至于如何处理任务的细节问题,在这里并不重要,我们感兴趣的是**「如何表征不同调度策略的同步特性」**。 > 程序清单 6-1 串行的 Web 服务器: ```java /** * 一个单线程串行执行的 Web Server */ public class SingleThreadWebServer { public static void main(String[] args) throws IOException { ServerSocket socket = new ServerSocket(80); while (true) { // 接收客户端的请求 Socket connection = socket.accept(); // 以串行的形式处理请求 handleRequest(connection); } } // 具体对请求做处理的逻辑,在这里我们不需要关心 private static void handleRequest(Socket connection) { } } ``` `SingleThreadWebServer` 很简单,且在理论上是正确的,但在实际生产环境中的执行性能却很糟糕,因为它「每次只能处理一个请求」。 主线程在「接受连接 `accept`」与处理相关请求 `handleRequest` 等操作之间不断地交替运行。当服务器正在处理请求时,新到来的连接必须等待直到服务器处理完成上一次的请求,然后服务器再次调用 `accpt`,来接受这一次请求。如果处理请求的速度很快,并且 `handleRequest` 可以立即返回,那么这种方法是可行的,但是现实世界中的 Web 服务器的情况却并非如此。 **【↑也就是如果这个服务很简单,耗时很短,那么单线程串行的Web 服务器也是可行的】** 在 **「Web请求的处理」** 中包含了一组不同的运算与 I/O 操作。 服务器必须处理 套接字 I/O 以读取请求和写回响应,这些操作通常会由于 **「网络拥塞」** 或 **「连接性问题」**而被**「阻塞」**。 此外,服务器还可能处理 文件I/O 或者 数据库请求,这些操作同样会阻塞。 在单线程的服务器中,阻塞不仅会推迟当前请求的完成时间,而且还将彻底组织等待中的请求被处理。 如果请求**「阻塞」**的时间过长,用户将任务服务器是不可用的,因为服务器看似失去了响应。 同时,服务器的**「资源利用率」** 非常低,因为当单线程在等待 I/O 操作完成时, CPU 将处于空闲状态。 在服务器应用程序中,**「串行处理机制」**通常都无法提供**「高吞吐率」**或**「快速响应性」**。但也有一些**例外**:当任务数量很少且执行时间很长时,或者当服务器只为单个用户提供服务,并且该用户每次只发出一个请求时,但大多数服务器应用程序并不是按照这种方式来工作的。【在某些情况中,串行处理方式能带来「简单性」和 「安全性」。大多数 GUI 框架都通过单一的线程来串行地处理任务。 第9章 将再次减少串行模型】 #### 6.1.2 显示地为任务创建线程 通过为每个请求创建一个新的线程来提供服务,从而实现更高的响应性。 > 程序清单6-2 在 Web服务器中为每个请求都启动一个新的线程**(不要这么做)**: ```java // 为每个请求都创建一个线程来对其进行处理 public class ThreadPerTaskWebServer { public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(80); while (true) { final Socket connection = serverSocket.accept(); Runnable task = () -> handleRequest(connection); } } private static void handleRequest(Socket connection) { // 在这里做具体的业务逻辑处理 } } ``` `ThreadPerTaskWebServer` 在结构上类似之前的单线程版本 —— 主线程仍然不断地 交替执行 「接受外部链接」与 「分发请求」 这两个操作。 区别在于,对于每个连接,主循环都将创建一个新线程来处理请求,而不是在 「主循环」 中进行处理,由此可得到**3个主要结论**: - 任务处理过程从主线程中分离出来,到每个新创建的子线程中去,使得主循环能更快地重新等待下一个到来的连接,使得不必等待上一个连接处理完成就可以接受新的请求,提高了**「响应性」**。 - 任务可以**「并行处理」**,从而能同时服务多个请求。如果有「多个处理器」,或者由于某种原因被「阻塞」,例如 「等待I/O完成」,获取锁资源 或者资源可用性等,程序的吞吐量将得到提高。【也就是同时可以处理更多的任务】 - **任务处理代码必须是「线程安全」**的,因为当有多个任务时,会并发地调用这段代码。 在「正常负载情况下」,「为每个任务分配一个线程」的方法能提升「串行执行」的性能。只要请求到达速率不超出服务器的请求处理能力,那么这种方法可以同时带来 **「更快的响应性」** 和 **「更高的吞吐率」**。 #### 6.1.3 无限制创建线程的不足 可以想到,这样为每个请求都创建一个线程的方法肯定有诸多不妥,尤其是有大量请求时,那就需要创建大量的线程: - **线程生命周期的开销非常高。** 线程的创建与销毁并非没有代价,根据「平台」不同,实际的开销也有所不同,但线程的创建过程都会需要「时间」,「延迟处理的请求」,并且需要 「JVM」 和 「操作系统」提供一些辅助操作。如果请求的到达率非常高且请求的处理过程是轻量级的,大多数服务器应用程序就是这种情况,那么为每个请求创建一个线程这种操作将消耗大量的计算资源。 - **资源消耗。** 「活跃的线程」会消耗「系统资源」,尤其是内存。如果可运行的线程数量多于可用处理的数量,那么有些线程将「闲置」。大量的空余线程会占用许多「内存」,给「垃圾回收器」带来压力, 而且大量线程在竞争 CPU 资源时还将产生 「其他性能开销」。所以如果已经有足够多的线程使 CPU 保持忙碌状态,那么此时创建「更多的线程」反而会「降低」性能。 - **稳定性**。 在「可创建线程」的数量上存在着一个限制。这个限制随着「平台」不同而不同,并且受多个因素制约,包括「JVM 的启动参数」,「`Thread`构造函数中请求的「栈」大小」,以及「底层系统」 对线程的限制等①。 如果破坏了这些限制,那么很可能抛出 `OutOfMemoryError`异常,要想从这种错误中恢复过来很危险,更好的方法是通过「构造程序」 避免超过这些限制。 ①【在 32位的机器上,其中一个主要的 **「限制因素」** 是 **线程栈的 地址空间**。 每个线程都维护 **「两个」** 线程栈,一个用于 **「Java 代码」**,另一个用于 **「原生代码」**。通常 JVM 在「默认情况」下会生成一个 **「复合的栈」**,大小约为 0.5MB (可以通过 JVM 标志 `-Xss` 或者通过 `Thread` 的构造函数来修改这个值。)如果将 2^32(32位系统下内存的最大值) 除以每个栈的大小**,那么线程数量将被限制为 「几千」 到 「几万」**。其他的一些因素,例如操作系统的限制等,则可能施加更加严格的约束。】 **在一定范围内,增加线程可以提高系统的吞吐率,当超过这个阈值,再创建更多的线程只会降低程序的执行速度,过多的创建线程,则会使整个应用程序崩溃。** 要想避免这种危险,就需要对程序可以创建的线程数量进行「限制」,并且全面地测试应用程序,从而确保达到线程最大限制数量时,程序也不会因「耗尽资源」而崩溃。 「为每个任务分配一个线程」 这种方法的问题在于"没有限制可创建线程的数量,只限制了远程用户提交 HTTP 请求的速率。" 与其他 「并发危险」 一样,在原型设计和开发阶段,无限制的创建线程或许还能正常运行,当到了应用程序部署后并处于高负载下运行时,才会有问题不断地暴露出来。某个恶意用户或者过多的用户同时访问,都会使 Web 服务器的负载达到阈值,从而崩溃。 如果服务器需要提供 高可用性,并且在**「高负载」**情况下**「平缓地降低」**性能,那么这将是一个严重的故障。 ##### 6.1小结: 先是举了一个串行执行任务的例子,然后用了给每个用户请求都创建一个线程用来做业务处理的例子,好处是确实能提高吞吐率和响应速度,但是过多的创建线程会给程序带来巨大的损害,这是无法接受的,于是我们的焦点就是如何创建数量合适的线程,并且只能创建这么多,不能突破界限,于是下面就引入了 `Executor` 线程池。 ### 6.2 Executor 框架 **「任务」**是一组**「逻辑工作」**单元,而线程则是使「任务」**异步执行**的机制。之前已经分析过「两种」 通过线程来执行任务的策略: - 把所有任务在单个线程中串行执行。 - 将每个任务放在各自的线程中执行。 上面这两种方式都存在一些严格的限制:**「串行执行」**的问题在于及其糟糕的响应性和吞吐量,而**「为每个任务分配一个线程」**的问题在于**「资源管理的复杂性」**。 第五章中,我们通过「有界队列」来防止高负荷的应用程序耗尽内存。 「线程池」简化了线程的管理工作,并且 `java.util.concurrent` 提供了一种灵活的线程池作为 `Executor`框架的一部分。 在 Java 类库中,任务执行的主要抽象不是 `Thread`,而是 `Executor`,如下面的程序所示: > 程序清单6-3 `Executor` 接口: ```java public interface Executor { void execute (Runnable commoand); } ``` `Executor`为灵活且强大的**「异步任务执行框架」**提供了基础,该框架能支持多种不同类型的任务**「执行策略」**。它提供了一种标准的方法将任务的**「提交」**与**「执行」**过程解耦,使用 `Runnbale`来表示任务。 `Executor`的实现还提供了对**「生命周期」**的支持,以及**「统计信息收集」**、**「应用程序管理机制」**和**「性能监视」**等机制。 `Executor`基于**「生产者—消费者」**模式,提交任务的操作相当于**「生产者」**(生产待完成的工作单元),执行任务的线程则相当于 「消费者」(执行完这些工作单元)。如果要在程序中实现一个 **「生产者—消费者」**的设计,最简单的方式就是通过使用 `Executor`。 #### 6.2.1 示例:基于 Executor的Web服务器 基于 `Executor`构建 Web服务器 非常容易。在程序清单 6-4 中用 `Executor` 代替了 硬编码的线程创建过程。在这种情况下使用了一种标准的 `Executor`实现,即一个「固定长度」的线程池,可以容纳 100 个线程。 > 程序清单 6-4 基于线程池的 **Web服务器**: ```java // 使用线程池实现一个 Web服务器 public class TaskExecutionWebServer { private static final int N_THREADS = 100; // 创建一个线程上限为100的线程池 private static final Executor exec = Executors.newFixedThreadPool(N_THREADS); public static void main(String[] args) throws IOException { ServerSocket socket = new ServerSocket(80); while (true) { final Socket connection = socket.accept(); Runnable task = () -> handleRequest(connection); // 使用线程池执行任务 exec.execute(task); } } private static void handleRequest(Socket connection) { // request-handling logic here } } ``` 在 `TaskExecutionWebServer`中,通过使用 `Executor` 将请求处理任务的「提交」 与任务的实际 「执行」 解耦开,并且只需要采用另一种不同的 `Executor` 实现,就可以改变服务器的行为。 改变 `Executor` 实现或配置所带来的影响要远远小于改变「任务提交方式」所带来的影响。 通常,`Executor`的配置是一次性的,因此在部署阶段可以完成,而提交任务的代码却会不断地「扩散」到整个程序,增加了修改的难度。 我们可以很容易地将 `TaskExecutionWebServer`修改为类似 `ThreadPerTaskWebServer` 的行为,只需要使用为每个请求都创建新线程的线程池,编写这样的 `Executor`也很简单,如下所示: > 程序清单6-5 为每个请求启动一个新线程的 `Executor`: ```java // 为每个请求都创建一个线程的 Executor public class ThreadPerTaskExecutor implements Executor { @Override public void execute(Runnable command) { new Thread(command).start(); } } ``` 同样,还可以编写一个 `Executor`使 `TaskExecutionWebServer`的行为类似于单线程程序的行为 —— 以「同步」 的方式执行每个任务,然后再返回,如下所示: > 程序清单6-6 在调用线程中以同步方式执行所有任务的 `Executor`: ```java // 让线程池以单线程的串行形式执行任务 public class WithinThreadExecutor implements Executor { @Override public void execute(Runnable command) { command.run(); } } ``` #### 6.2.2 执行策略 通过将任务的**「提交」**与 **「执行」** 解耦,从而无需太大的困难就可以为某种类型的任务指定和修改执行策略。在执行策略中定义了任务执行的**「What」**、**「Where」**、**「When」**、**「How」**等方面,具体意义如下: - 在什么(What)线程中执行任务? - 任务按照什么(What)顺序执行(FIFO、LIFO、优先级)? - 有多少个(How Many)任务能「并发」执行? - 在队列中有多少个(How Many)任务在等待执行? - 如果系统由于「过载」而需要拒绝一个任务,那么应该选择哪一个(Which)任务?另外,如何(How)通知应用程序有任务被拒绝? - 在执行一个任务之前或之后,应该进行哪些(What)动作? 各种**「执行策略」**都是一种**「资源管理工具」**,最佳策略取决于**「可用的计算资源」**以及**「服务质量的需求」**。 通过**「限制并发任务的数量」**,可以确保应用程序不会由于**「资源耗尽」**而失败,或者由于在稀缺资源上发生竞争而严重影响性能(这类似于某个企业应用程序中**「事务监视器(Transaction Monitor)」**的作用:它能将事务的执行速率控制在某个合理水平,因而就不会使资源耗尽,或者对系统造成过大压力。) 通过将任务的提交与执行的策略分离开,有助于在**「部署阶段」**选择与「**可用硬件资源」**最匹配的执行策略。 > 每当看到下面这种形式的代码时: > > `new Thread(runnable).start()` > > 如果你希望获得一种更灵活的执行策略时,请考虑使用 `Executor`来代替直接使用 `Thread`。 #### 6.2.3 线程池 **「线程池」**指管理一组**「同构」**工作线程的资源池。线程池与**「工作队列(Work Queue)」**关系密切。 在工作队列中保存了所有等待执行的任务。**「工作者线程(Work Thread)」**的任务很简单:从工作队列中获取一个任务,执行任务,然后返回线程池并等待下一个任务。 在线程池中执行任务 与 "为每个任务分配一个线程"有众多优势。首先重用线程而不是创建新线程,可以在处理多个请求时「分摊」线程创建和销毁过程中的巨大开销。另外当用户请求到达时,线程已经被创建好,因此省去了等待线程创建的时间,提高了「响应性」。 通过适当调整线程池的大小,可以创建足够多的线程以便使处理器保持忙碌姿态,同时还可以「防止过多」线程相互竞争资源而使应用程序「耗尽内存」或「失败」。 类库提供了一个灵活的线程池以及一些有用的「默认配置」。可以通过 Executors 中的静态工厂方法之一来创建一个线程池,下面是不同的线程池类型: - `newFixedThreadPool`,创建一个固定长度的线程池,每提交一个任务该线程池中就创建一个线程,直到达到线程池「最大数量」。这时线程池的规模将不再变化(如果有线程在执行时遇到「未预期」的 `Exception`而结束,那么线程池会补充一个新的线程) <---【也就是对于意外减员的应对情况】 - `newCachedThreadPool`,创建了一个「可缓存」的线程池,如果线程池的当前规模超过了「处理器的需求」,那么将「回收」空闲的线程,而当需求增阿基时,则可以添加新的线程,「该线程池的规模不存在任何限制」。<---【那么是否存在线程创建过多导致资源耗尽的问题?】 - `newSingleThreadExecutor`,一个单线程的线程池,它创建单个工作线程来执行任务,如果这个线程「异常结束」,会创建另一个线程进行代替。`newSingleThreadExecutor`能确保依照任务在队列中的顺序来「串行」执行(例如FIFO,LIFO,优先级) 单线程的 `Executor`提供了大量的「内部」同步机制,从而确保了任务执行的任何内存写入操作对于后续任务来说都是「可见」的。这意味着,即使这个线程会时不时被「另一个线程替代」,但对象总是可以「安全」地「封闭」在「任务线程」 中。 - `newScheduledThreadPool`,创建了一个固定长度的线程池,而且以「延迟」或定时的方式来执行任务,类似于 `Timer`。 `newFixedThreadPool` 和 `newCachedThreadPool` 这两个工厂方法返回「通用」 的 `ThreadPoolExecutor` 实例,这些实例可以直接用来构造专门用途的 `executor`。 将在 第8章 中深入讨论「线程池的各个配置选项」。 `TaskExecutionWebService` 中的 Web服务器使用了一个带有 有界线程池 的 `Executor`。通过 `execute` 方法将任务提交到工作队列中,工作线程反复地从工作队列汇总取出并执行它们。 从"**为每个任务分配一个线程**"策略 改变为 **基于线程池**的策略,对应用程序的「稳定性」将产生重大的影响,使用线程池的情况下:Web服务器不会再在高负载情况下失败。(尽管使用线程池服务器不会因为创建了过多的线程而失败,但在足够长的时间内,如果任务的「到达速度」总是超过「执行速度」,那么服务器仍然有可能耗尽内存,因为等待执行的 `Runnable`队列将不断增长。可以通过使用一个**「有界工作队列」** 在 Executor 框架内部解决这个问题。) **由于服务器不会创建数千个线程来「争夺」有限的 CPU 和内存资源,因此服务器的性能将平缓地降低。**通过使用 `Executor`,可以实现各种**「调优」**、**「管理」**、**「监视」**、**「记录和日志」**、**「错误报告」**和其他功能。如果不使用 **「任务执行框架」**,要增加这些功能是非常困难的。 #### 6.2.4 Executor 的生命周期 之前已经说了如何「创建」一个 `Executor`,但是没有讨论如何关闭它。 `Executor` 的实现通常会创建线程来执行任务,但 `JVM` 只有在所有(非守护)线程全部终止之后才会退出。 **「因此,如果无法正确地关闭 `Executor`,那么 `JVM` 将无法结束。」** <---【所以关闭任务执行框架非常的重要】 由于 `Executor`以 「异步」 方式来执行任务,因此在任何时刻,之前提交任务的状态不是「立即可见」的。有些任务可能已经完成,有些可能正在运行,而其他的任务可能在队列中等待执行。 当关闭应用程序时,可能使用的是「最平缓」的关闭形式(完成所有已经启动的任务,并且不再接受「任何形式」 的新任务),也可能采用的是「最粗暴」的关闭形式 —— 直接关掉机房的电源,以及其他各种可能的形式。 既然 `Executor`是为应用程序提供服务的,所以它们也是可关闭的(无论是采用平缓的还是粗暴的方式),并将在关闭操作中「受影响的任务」的状态反馈给「应用程序」。 为了解决 任务执行框架 `Executor` 的生命周期问题,`ExecutorService`扩展了 `Executor` 接口,(这里翻译有一个**重大问题**,明明 `ExecutorService` 是对 `Executor` 的扩展,原书把关系写反了翻译成了 `Executor` 是对 `ExcutorService` 的扩展。原文:**[the ExecutorService interface extends Executor** , adding a number of methods for lifecycle management])添加了一些用于「管理」 生命周期的方法(还有一些用于提交任务的便利方法),具体如下: ```java public interface ExecutorService extends Executor { void shutdown(); List