《Java并发编程实战》 任务执行和取消关闭

来源:互联网 发布:中国2016年m2数据 编辑:程序博客网 时间:2024/05/01 22:26

 Java没有提供任何机制来安全地(抢占式方法)终止线程,虽然Thread.stop和suspend等方法提供了这样的机制,但是由于存在着一些严重的缺陷,因此应该避免使用。但它提供了中断Interruption机制,这是一种协作机制,能够使一个线程终止另一个线程的当前工作。

一、任务取消

取消操作的原因:
. 用户请求取消
. 有时间限制的操作
. 应用程序事件
. 错误
. 关闭

结束任务的四种方式:
1. run方法执行结束
2. 使用请求关闭标记(例如boolean开关)
3. 使用中断机制
4. 使用Future退出方法


2. 使用请求关闭标记
     当执行到并满足条件是使用return退出run方法
     变量需要volatile确保变量多线程环境下的可见性。
     -没有执行到判断条件就不会退出,所以不是立即退出的办法。

3. 使用中断机制
     优点是相对“请求关闭标记”相应更快一些,但也不是立即关闭线程。
     void        interrupt()       中断线程。
     boolean   interrupted()   测试当前线程是否已经中断。
     boolean   isInterrupted() 测试线程是否已经中断。
     InterruptedException异常

程序应该对线程中断作出恰当的响应。

4. 使用Future退出方法

boolean cancel(boolean mayInterruptIfRunning)  
          试图取消对此任务的执行。 
boolean isCancelled()
          如果在任务正常完成前将其取消,则返回 true。 


*. 处理不可中断的阻塞
*. 采用newTaskFor来封装费标准的取消


二、停止基于线程的服务

    之前的任务取消,主要是涉及如何关闭单个线程并且都是由创建单个线程的对象来进行关闭操作,但是如果线程不是由对象自己而是由线程池统一创建的线程该如何处理呢?
      1. 使用线程的对象进行关闭 - 当前即使不在对象中创建线程而由线程池创建,这个对象依然可以关闭线程,这点一定要相信程序员的破坏能力,只是使用第2种方式更符合封装原则。(设置标志,逐步取消)
      2. 使用线程池统一管理 - 如果是使用ExecutorService创建就交由其进行关闭操作。

2. 使用线程池统一管理(关闭ExecutorService)
void shutdown()
     启动一次顺序关闭,执行以前提交的任务,但不接受新任务。如果已经关闭,则调用没有其他作用。 
     -- 安全关闭方式。
List<Runnable> shutdownNow()
          试图停止所有正在执行的活动任务,暂停处理正在等待的任务,并返回等待执行的任务列表。 
          无法保证能够停止正在处理的活动执行任务,但是会尽力尝试。例如,通过 Thread.interrupt() 来取消典型的实现,所以任何任务无法响应中断都可能永远无法终止。 
          -- shutdownNow方法的局限性,强制关闭方式。
boolean isShutdown()
     boolean isShutdown()如果此执行程序已关闭,则返回 true。 

3. “毒丸”对象
只有在生产者消费者的数量都已知的情况下,才可以使用“毒丸”对象。

三、处理非正常的线程终止
    Thread.UncaughtExceptionHandler全局的捕获的异常处理,通常在应用中用于异常的统计,收集到这些统计后可以对应用进行异常修复。


第六章 任务执行

大多数并发应用程序是围绕执行任务进行管理的。设计任务时,要为任务设计一个清晰的任务边界,并配合一个明确的任务执行策略。任务最好是独立的,因为这会提高并发度。大多数服务器应用程序都选择了下面这个自然的任务边界:单个客户请求。
任务时逻辑上的工作单元,线程是使任务异步执行的机制。
应用程序内部的任务调度,存在多种可能的调度策略:
其中,最简单的策略是在单一的线程中顺序的执行任务。但它的吞吐量和响应性很差,一般只在特殊情况下使用:任务的数量很少但生命周期很长时,或者服务器只服务于唯一的用户时,服务器在同一时间内只需同时处理一个请求。
每任务一个线程(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框架可以简化构建可取消的任务和服务。






五、参考资料:

"程序应该对线程中断作出恰当的响应" 摘录自《温绍锦 - Java并发程序设计教程》
0 0
原创粉丝点击