序: 任务和线程的启动很容易。在大多数时候,我们会让他们运行直到结束,或者让他们自行停止。然而有时候我们希望提前结束任务或者线程,获取因为用户取消了操作,或者用户程序需要被快速关闭。
1、任务取消
1.1、中断
public class Thread {
//能中断目标线程
public void interrupt() {...}
//返回中断线程的目标状
public boolean isInterrupted() {...}
//清除当前线程的中断状态,并返回它之前的值,这也是清除中断状态的唯一方法
public static boolean interupted() {...}
}
理解:它并不会真正中断一个正在运行的线程,而只是发出中断请求,然后由线程在下一个合适的时刻中断自己。(这些时刻也被称为取消点)
有些方法,例如wait、sleep和join等,将严格处理这些请求,当他们收到中断请求或者在开始执行时发现某个已被设置好的中断状态时,将抛出一个异常。(InterruptedException)
在JAVA的API或者语言规范中,并没有将中断与任何取消语义关联起来,但实际上在取消之外的其它操作中使用中断的话都是不合适的,并且很难支撑起更大的系统。通常,中断是实现取消的最合理方式。
1.2、中断策略
当发现中断请求时,应该做哪些操作。
线程应该只能由其所有者中断,所有者可以将线程的中断策略信息封装到某个合适的取消机制中,例如shutdown方法。每个线程拥有各自的中断策略,除非你知道中断对此线程意味着什么,否则就不应该中断该线程。
如果除了将InterruptedException传递给调用者外还需要执行其它操作,那么应该在捕获InterruptedException后恢复中断状态:Thread.currentThread().interrupt();
1.3、通过Future来实现取消
当Future.get()抛出IinterruptedException或者TimeoutException时,如果你知道不再需要结果,那么就可以调用Future.cancel来取消任务。
1.4、采用newTaskFor来封装非标准的取消
Java6在ThreadPoolExecutor中新增的功能。当把一个Callable提交给ExeceutorService时,submit方法会返回一个Future,我们可用通过这个Future来取消任务。newTaskFor是一个工厂方法,它将创建Future来代表任务。newTaskFor还能返回一个RunableFuture接口,该接口扩展了Future和Runable(并由FutureTask实现)。通过定制表示任务的Future可以改变Future.cancel的行为。
2、停止基于线程的服务
应用程序通常会创建拥有多个线程的服务,如线程池,并且这些服务端的生命周期通常比创建他们的方法的生命周期更长。如果应用程序准备退出,如果应用程序退出,那么这些服务所拥有的线程也需要结束。
正确的封装原则时:除非拥有某个线程,否则不能对该线程进行操控。例如中断线程或者修改线程的优先级等。线程有一个相应的所有者,即创建该线程的类。因此线程池是其工作线程的所有者,如果要中断这些线程,那么应该使用线程池。
与其它封装对象一样,线程的所有权是不可传递的。应用程序拥有服务,服务拥有工作者线程,但应用程序不能拥有工作者线程,因此应用程序不能直接停止线程。相反服务应该提供生命周期来关闭它自己以及它拥有的线程。
2.1、关闭ExecutorService
使用shutdown正常关闭,速度慢,更安全,因为ExecutorService会一直等到队列中的所有任务都执行完后才关闭。
使用shutdownNow强行关闭,首先关闭当前正在执行的任务,然后返回所有尚未执行的任务清单。强行关闭速度块,风险高,因为任务有可能在执行一半时就被关闭了。然而TrackingExecutor可以找出哪些任务已经开始但还没有正常完成。在Exectutor结束后,getCancekedTasks返回被取消的任务清单,用使这项技术发挥作用,任务在返回时必须维持线程的中断状态。
2.2、“毒丸”对象
“毒丸”是指一个放在队列上的对象,当得到这个对象时,立即停止。只有在生产者和消费者数量都已知的情况下才能使用毒丸对象。
3、处理非正常的线程终止
某个线程发生了未捕获的异常而终止。可以考虑使用try-catch代码块来调用这些任务,或者也可以使用try-finally代码块调用这些任务。
当一个线程由于未捕获异常而退出时,JVM会把这个事件报告给应用程序提供的UncaughtExceptionHandler,它能检测出某个线程由于未捕获的异常而终结的情况。
public interface UncaughtExceptionHandler {
void uncaughtException (Thread t, Throwable e);
}
只有通过execute提交的任务,才能将它抛出的异常交给未捕获异常处理器,而通过submit提交的任务,无论抛出未检查异常还是已检查异常,都将被认为是任务返回状态的一部分。如果一个由submit提交的任务抛出了异常而结束,那么这个异常将被Future.get封装在ExecutionException中重新抛出。
4、JVM关闭
JVM既可以正常关闭,也可以强行关闭。
4.1、关闭钩子
在正常关闭中,JVM首先调用所有已注册的关闭钩子。关闭钩子是指通过Runtime.addShutdownHook注册的但尚未开始的线程。当所有的关闭钩子都执行结束时,如果runFinalizersOnExit为true,那么JVM将运行终接器,然后再停止。关闭钩子应该是线程安全的。
//通过注册一个关闭钩子来停止日志服务
public void start() {
Runtime.getRuntime().addShutdownHook(
new Thread() {
public void run() {
try{
LogService.this.stop();
} catch(InterruptedException ignored) {
}
}
}
);
}
4.2、守护线程
线程分为普通线程和守护线程,两者之间差异在于线程退出时发生的操作。当一个线程退出时,JVM会检查其它正在运行的线程,如果这些线程都是守护线程,那么JVM会正常退出操作。当JVM停止时,所有的守护线程都会被抛弃(即不会执行finally块,也不会执行回卷栈,而JVM只是直接退出)。
4.3、终结器
避免使用终结器。
参考《Java并发编程实战》