首页 > 代码库 > 《Java并发编程实战》学习笔记Ⅱ
《Java并发编程实战》学习笔记Ⅱ
第六章 任务执行
大多数并发应用程序是围绕执行任务进行管理的。设计任务时,要为任务设计一个清晰的任务边界,并配合一个明确的任务执行策略。任务最好是独立的,因为这会提高并发度。大多数服务器应用程序都选择了下面这个自然的任务边界:单个客户请求。
任务时逻辑上的工作单元,线程是使任务异步执行的机制。
应用程序内部的任务调度,存在多种可能的调度策略:
其中,最简单的策略是在单一的线程中顺序的执行任务。但它的吞吐量和响应性很差,一般只在特殊情况下使用:任务的数量很少但生命周期很长时,或者服务器只服务于唯一的用户时,服务器在同一时间内只需同时处理一个请求。
每任务一个线程(thread-per-task)。在中等强度的负载下,“每任务一个线程”的方法是对顺序化执行的良好改进。但它存在一些实际的缺陷,因为它会无限制的创建线程,创建/关闭线程是需要开销的,同时线程还会消耗系统资源,而且会影响稳定性。所以应该限制可创建线程的数目。
使用线程池——Executor框架。如同有界队列,Executor可以防止应用程序过载而耗尽资源,而且Executor是基于生产者-消费者模式的,可以分离任务提交和任务执行。如果要在你的程序中实现一个生产者-消费者的设计,使用Executor通常是最简单的方式。
使用Executor的一个优点是:要改变程序的运行,只要改变Executor的实现就行,也就是任务的执行,不需要动任务的提交,而且Executor的实现是放在一个地方的,但任务的提交则是扩散到整个程序中。
执行策略是资源管理工具,最佳策略取决于可用的计算资源和你对服务质量的需求。一个执行策略指明了任务执行的“what,where,when,how”几个因素,具体包括:
任务在什么(what)线程中执行?
任务以什么(what)顺序执行(FIFO,LIFO,优先级)?
可以有多少个(how many)任务并发执行?
可以有多少个(how many)任务进入等待执行队列?
如果系统过载,需要放弃一个任务,应该挑选哪一个(which)任务?另外,如何(how)通知应用程序知道这一切呢?
在一个任务的执行前与结束后,应该做什么(what)处理?
Executor的生命周期
Executor有三种状态:运行、关闭、终止。创建后的初始状态是运行状态,shutdown()方法会启动一个平缓的关闭过程,shutdownNow()方法会启动一个强制的关闭过程。在关闭后提交到Executor中的任务,会被被拒执行处理器(RejectedExecutionHandler)处理(可能只是简单的放弃)。一旦所有的任务全部完成后,Executor回转入终止状态,可以调用awaitTermination等待,或者isTerminated判断。
可以使用scheduledThreadPoolExecutor代替Timer使用,Timer存在一些缺陷:Timer只创建唯一的线程来执行所有timer任务;Timer抛出的未检查的异常会终止timer线程,而且Timer也不会再重新恢复线程的执行了。
Executor框架让制定一个执行策略变得简单,不过想要使用Executor,你还必须能够将你的任务描述为Runnable。
Runnable、Callable、Future比较
Runnable是执行任务的抽象,但它的run方法不能返回一个值或者跑出受检查的异常;Callable是更佳的抽象,它的run方法有返回值,并能抛出异常;而Future提供了相关的方法来获得任务的结果、取消任务以及检验任务是否已经完成还是被取消。
Executor的所有submit方法都返回一个Future,用它可以重新获得任务执行的结果,或者取消任务。除此之外,在Java 6中,ExecutorService的所有实现都可以重写newTaskFor方法,把Callable封装成Future。
将程序的任务量分配到不同的任务中:当存在大量的相互独立、同类的能够并发处理的任务时,性能才能真正的提升;否则,性能提升的相当少,甚至降低性能。
当有一批任务需要Executor处理时,使用completionService更方便,而且还可以使用take方法,获取完成的任务(可以没完成一个取一个,提高并发)。如果不需要边完成边去结果的话,处理批任务还可以使用Executor.InvokeAll方法。
总结
围绕任务的执行来构造应用程序,可以简化开发,便于同步。Executor框架有助于分离任务的提交和任务的执行策略,同时还支持很多不同类型的执行策略。每当你要为执行任务而创建线程时,可以考虑使用Executor。为了最大化效益,在把应用程序分解为不同的任务时,你必须确定一个合乎情理的任务边界。在一些应用程序中,存在明显的工作良好的任务边界,然而还有一些程序,你需要作进一步的分析,以揭示更多可行的并发。
第七章 取消和关闭
任务取消:当外部代码能够在活动自然完成之前,把它更改为完成状态,那么这个活动被称为可取消的。活动取消的原因:用户请求取消、限时活动、应用程序事件、错误、关闭。
Java没有提供任何机制,来安全的强迫线程停止手头的工作。它提供了中断——一个协作机制,是一个线程能够要求另一个线程停止当前的工作。任务和服务可以这样编码:当要求它们停止时,它们首先清除当前进程中的工作,然后再终止。因而需要一个取消策略。
取消策略,取消的how、when、what:其他代码如何请求取消该任务,任务在什么时候检查取消的请求是否到达,响应取消请求的任务中应有的行为。
在API和语言规范中,并没有把中断与任何取消的语意绑定起来,但是,实际上,使用中断来处理取消之外的任何事情都是不明智的,并且很难支撑起更大的应用。
调用interrupt并不意味着必然停止目标线程正在进行的工作;它仅仅传递了请求中断的消息。我们对中断本身最好的理解应该是:它并不会真正中断一个正在运行的线程;它仅仅发出中断请求,线程自己会在下一个方便的时刻中断(取消点)。
中断通常是实现取消最明智的选择。
因为每一个线程都有其自己的中断策略,所以你不应该中断线程,除非你知道中断对这个线程意味着什么。
调用可中断的阻塞函数时,如Thread.sleep、BlockingQueue.put,有两种处理InterruptedException的实用策略:
传递异常(很可能发生在清除特定任务后),使你的方法也成为可中断的阻塞方法;
或者恢复中断状态,从而上层调用栈中的代码能够对其进行处理。
只有实现了线程中断策略的代码才可以接受中断请求。一般性的任务和程序库代码不应该接受中断请求。
当Future.get抛出InterruptedException或TimeoutException时,如果你知道不再需要结果时,就可以调用Future.cancel来取消任务了。
被不可中断活动阻塞的线程,我们可以用类似于中断的技术停止它们,但这更需要明确线程阻塞的原因。
对于拥有线程的服务,只要服务的生存时间大于创建线程的方法的生存时间,就需要提供生命周期方法。
生产者-消费者服务关闭的方法:
自己提供生命周期方法,生产者原子的添加工作,比较难。
使用ExecutorService类:封装ExecutorService,在内部代码中调用ExecutorService的生命周期方法——shutdown、shutdownNow。在非生产者-消费者中也适用。
使用毒丸:一个可识别的对象,置于队列中,意味着“当你得到它时,停止一切工作”。要停止服务时,中断生产者——生产者的中断处理中向每一个消费者添加一个毒丸,消费者碰到毒丸,停止工作。
如果一个方法需要处理一批任务,并在所有任务结束前不会返回,那么他可以通过使用私有的Executor来简化服务的生命周期管理,其中Executor的生命限定在该方法中。
shutdownNow的局限性:它试图取消正在进行的任务,并返回那些等待执行的任务的清单,但是我们没法找出那些已经开始执行、却没有结束的任务,这需要自己处理。
在一个长时间运行的应用程序中,所有的程序都要给未捕获异常设置一个处理器,这个处理器至少要将异常信息记入日志中。
线程分为两种:普通线程和守护线程。两者的区别是:当一个线程退出时,所有仍然存在的守护线程都会被抛弃——不会执行finally块,也不会释放栈——JVM直接退出。
管理资源避免使用finalizer。在大多数情况下,使用finally块和显式close方法结合来管理资源,会比使用finalizer起到更好的作用。
总结
任务、线程、服务以及应用程序在生命周期结束时的问题,可能会导致向它们引入复杂的设计和实现。Java没有提供具有明显优势的机制来取消活动或者终结线程。它提供了协作的中断机制,能够用来帮助取消,但是这将取决你如何构建取消的协议,并是否能一致的使用该协议。使用FutureTask和Executor框架可以简化构建可取消的任务和服务。