并发编程学习笔记之取消和关闭(六)
小节
为什么需要取消和关闭: 有时候我们希望在任务或线程自然结束之前就停止它们,可能因为用户取消了操作,或者应用程序需要快速关闭.
取消和关闭的好处: 不会浪费资源执行一些没用的操作、保证程序的正常退出.
Java没有提供任何机制,来安全地强迫线程停止手头的工作.它提供中断(线程的interrupt方法)--- 一个协作机制,使一个线程能够要求另一个线程停止当前的工作.
立即停止线程的坏处:这种协作方法是必须的,因为我们很少需要一个任务、线程或者服务立即停止,立即停止会导致共享的数据结构处于不一致的状态.任务和服务可以这样编码:当要求它们停止时,它们首先会清除当前进程中的工作,然后再终止.这提供了更好的灵活性,因为任务代码本身比发出取消请求的代码更明确应该清除什么.
生命周期结束的问题使任务、服务以及程序的设计和实现变得复杂起来,这个程序设计中非常重要的元素却经常被忽略.处理好失败、关闭和取消是好的软件和勉强运行的软件最大的区别.(不销毁执行线程,JVM无法退出)
任务取消
当外部代码能够在活动自然完成之前,把它更改为完成状态,那么这个活动被称为可取消的(cancellable).我们可能因为很多原因取消一个活动.
-
用户请求的取消
-
限时活动
-
应用程序事件,一个任务发现了解决方案,所有其他仍在工作的搜索会被取消
-
错误. 发生错误的时候,可能之前所有的任务都被取消
-
关闭. 一个优雅的关闭,可能允许当前的任务完成;在一个更加紧迫的关闭中,当前的任务就可能被取消了.
在Java中,没有哪一种用来停止线程的方式是绝对安全的,因此没有哪一种方式优先用来停止任务.这里只有选择相互协作的机制,通过协作,使任务和代码遵循一个统一的协议,用来请求取消.
在这些协作机制中,有一种会设置"cancellation requested",任务会定期查看;如果发现标志被设置过,任务就会提前结束.
public class cancellation {
//退出循环的标识符, volati保证可见性
private volatile boolean identify = true;
public void cycle(){
while(identify){
System.out.println("持续输出");
}
}
public void stop(){
identify = false;
}
}
执行线程在关闭程序的时候必须被取消,否则导致JVM不能正常退出.
一个可取消的任务必须拥有取消策略(cancellation policy),这个策略详细说明关于取消的"how"、"when"、"what"---其它代码如何请求取消任务,任务在什么时机检查取消的请求是否到达,响应取消请求的任务中应有的行为.
中断
上面的例子存在致命的缺陷:正常情况下,执行循环,打印输出语句,可以正确的检查标识符状态并退出循环,但是如果把输出语句换成一个会阻塞的方法,例如BlockingQueue.put.那么就存在方法被阻塞住了.导致永远无法退出循环的可能.
public class Cancellation {
//退出循环的标识符, volati保证可见性
private volatile boolean identify = true;
private final BlockingQueue<String> queue = new ArrayBlockingQueue<>(1);
public void cycle(){
while(identify){
try {
//阻塞住了,无法执行到while判断,永远无法退出循环
queue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void stop(){
identify = false;
}
}
好在queue.take()会响应中断,当你能获取到queue.take()的执行线程,并调用.interrupt()方法的时候,会结束阻塞并捕获InterruptedException.
特定阻塞库类的方法支持中断,线程中断是一个协作机制,一个线程给另一给线程发送信号(signal),通知它在方便或者可能的情况下停止正在做的工作,去做其他事情.
在API和语言规范中,并没有把中断与任何取消的语意绑定起来,但是实际上,使用中断来处理取消之外的任何事情都是不明智的,并且很难支撑起更大的应用
每一个线程都有一个boolean类型的中断状态(interrupted status):在中断的时候,这个中断状态被设置为true.
证明阻塞库类可以响应线程中断代码:
public class Cancellation {
private final BlockingQueue<String> queue = new ArrayBlockingQueue<>(1);
public void cycle(){
try {
queue.take();
System.out.println("如果输出这句话代表没阻塞");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String [] args) throws InterruptedException {
Cancellation c = new Cancellation();
Thread t = new Thread(() -> c.cycle());
t.start();
Thread.sleep(1000);
t.interrupt();
//查看中断状态
t.isInterrupted();
}
}
输出:
java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.reportInterruptAfterWait(AbstractQueuedSynchronizer.java:2014)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2048)
at java.util.concurrent.ArrayBlockingQueue.take(ArrayBlockingQueue.java:403)
at cn.bj.lbr.test.chap7.Cancellation.cycle(Cancellation.java:25)
at cn.bj.lbr.test.chap7.Cancellation.lambda$main$0(Cancellation.java:36)
at java.lang.Thread.run(Thread.java:748)
interrupt方法中断目标线程,并且isInterrupted返回目标线程的状态.
静态的通过Thread.interrupted调用的方法仅能够清除当前线程的中断状态,并返回他之前的值:这是清除中断状态唯一的方法.
阻塞库函数例如Thread.sleep和Object.wait等,它们对中断的响应表现为:清除中断状态,抛出InterruptedException;这表示阻塞操作因为中断的缘故提前结束.
当线程在并不处于阻塞状态的情况下发生中断时,会设置线程的中断状态,然后一直等到被取消的活动获取中断状态,来检查是否发生了中断.如果不触发InterruptedException,中断状态会一直保持,直到有人特意去清除中断状态.
调用interrupt并不意味着必然停止目标线程正在进行的工作;它仅仅传递了请求中断的消息.
我们对中断本身最好的理解应该是:它并不会真正中断一个正在运行的线程:它仅仅发出中断请求,线程自己会在下一个方便的时候中断(这些时刻被称为取消点,cancellation point).有一些方法对这样的请求很重视,比如wait、sleep和join方法,当它们接到中断请求时会抛出一个异常,或者进入时中断状态就已经被设置了.
Thread.interrupted(线程的静态方法)应该小心使用,因为他会清除并发线程的中断状态.如果你调用了interrupted,并且它返回了true,你必须对其进行处理,除非你想掩盖这个中断-- 你可以抛出InterruptedException,或者通过在次调用interrupt来保存中断状态.
如果你的任务代码响应中断,不要使用自定义的退出标识,使用线程的中断作为你的取消机制,这样在代码阻塞的时候你依然可以退出任务.示例:
public class Cancellation {
private final BlockingQueue<String> queue = new ArrayBlockingQueue<>(1);
public void cycle(){
try {
//使用线程的中断状态作为取消的标识符,保证在阻塞的时候依然可以取消任务
while(!Thread.currentThread().isInterrupted()) {
queue.take();
System.out.println("如果输出这句话代表没阻塞");
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String [] args) throws InterruptedException {
Cancellation c = new Cancellation();
Thread t = new Thread(() -> c.cycle());
t.start();
Thread.sleep(1000);
t.interrupt();
boolean interrupted = Thread.interrupted();
}
}
上述代码调用了可中断的take(),因此显式的while(!Thread.currentThread.isInterrupted)不是绝对必要的,但是这种检测会使代码对中断具有更好的响应性.这是因为它在耗时的任务之前就检查了中断.而不是在任务完成之后才去检查.当我们调用可中断的阻塞方法时.通常并不能得到期望的响应,对中断状态进行显示的检测会对此有一定的帮助.
中断通常是实现取消最明智的选择
中断策略
正如需要为任务制定取消策略一样,也应该制定线程中断策略(interruption policy).
一个中断策略决定线程如何应对中断请求---当发现中断请求时,它会做什么(如果确实响应中断的话),哪些工作单元对于中断来说是原子的,以及在多快的时间里响应中断.
线程池实现以外的代码应该传递异常或者保存中断状态:代码如果并不是线程的所有者(对于线程池而言,是指任何线程池实现以外的代码)就应该小心地保存中断状态,这样所有者的代码才能够最终对其起到作用,甚至是"访客"代码也能起到作用.
这就是为什么大多数可阻塞的库函数,仅仅抛出InterruptedException作为中断的响应.它们绝不可能运行在一个线程中,所以它们为任务或者库代码实现了大多数合理的取消策略:它们会尽可能快地为异常信息让路,把它们向后传给调用者,这样上层栈的代码就可以进一步行动了.
当检查到中断请求时,任务并不需要放弃所有事情---它可以选择推迟,直到更适合的时机.这需要记得它已经被请求过中断了,完成当前正在进行的任务,然后抛出InterruptedException或者指明中断.当更新的过程中发生中断时,这项技术能够保证数据结构不被彻底破坏.
无论任务把中断解释为取消,还是其他的一些关于中断的操作,它都应该注意保存执行线程的中断状态.如果对中断的处理不仅仅是把InterrutptedException传递给调用者,那么它应该在捕获InterruptedException之后恢复中断状态.
try {
//do somethings
} catch (InterruptedException e) {
e.printStackTrace();
//保存中断状态
Thread.currentThread().interrupt();
}
线程应该只能被线程的所有者中断:所有者可以把线程的中断策略信息封装到一个合适的取消机制中,比如关闭(shutdown)方法中.
因为每一个线程都有其自己的中断策略,所以你不应该中断线程,除非你知道中断这个线程意味着什么.
响应中断
当你调用可中断的阻塞函数时,比如Thread.sleep或者BlockingQueue.put,有两种处理InterruptedException的使用策略
- 传递异常(在方法名后面 throws InterrutedException,在调用这个方法的地方处理这个异常)
- 保存中断状态,上层调用栈中的代码能够对其进行处理
如果你不想,或不能传递InterruptedException(例如在Runnable中),你就应该在捕获异常后调用Thread.currentThread.interrupt恢复中断状态.你不应该掩盖InterruptedException,在catch快中捕获到异常缺什么也不做.
只有实现了线程中断策略的代码才可以接收中断请求,通用目的的任务和库的代码绝不应该接受中断请求.
在任务的外部线程中安排中断:
public class InterruptTest {
//定任任务
private static final ScheduledExecutorService scheduledExecutorService= Executors.newScheduledThreadPool(1);
public static void timeRun(Runnable r, Long timeOut, TimeUnit timeUnit){
//得到当前线程
Thread t = Thread.currentThread();
//定时任务,超过参数设定的时间中断当前线程.
scheduledExecutorService.schedule(new Runnable() {
@Override
public void run() {
t.interrupt();
}
},timeOut,timeUnit);
//执行任务
r.run();
}
}
示例代码是错误的,本意是r.run()超过限定的时长终止任务,但是如果r.run()已经执行完毕,定时任务启动,就会导致终止了当前线程.但是此时线程执行什么样的代码是无法确定的,此时中断肯定是错误的.
改正版:
public class InterruptTest {
//定任任务
private static final ScheduledExecutorService scheduledExecutorService= Executors.newScheduledThreadPool(1);
public static void timedRun(Runnable r,Long timeOut,TimeUnit timeUnit) throws InterruptedException {
//内部类,自定义一个run方法
class RethrowableTask implements Runnable{
private volatile Throwable t;
@Override
public void run() {
try {
//方法传递进来的r
r.run();
}catch (Throwable t){
this.t = t;
}
}
void rethrow() throws Throwable {
if(t != null){
throw t;
}
}
}
RethrowableTask task = new RethrowableTask();
//创建新的本地线程.用它来终止任务.
Thread t = new Thread(task);
t.start();
//超过时长中止线程
scheduledExecutorService.schedule(new Runnable() {
@Override
public void run() {
t.interrupt();
}
}, timeOut, timeUnit);
//t.join(),()内的时间参数就是让t线程单独执行的时间.在此期间内其他线程会阻塞
t.join(timeUnit.toMillis(timeOut));
try {
task.rethrow();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
}
改进的地方就是将执行任务的线程改为一个新的本地线程,这样终止这个线程不会影响到其他任务.
通过Future取消
最终的结论: 取消任务就用Future.
Future可以管理任务的生命周期,处理异常,并有利于取消.
ExecutorService.Submit会返回一个Future来描述任务.Future有一个cancel方法,它需要一个boolean类型的参数,mayInterruptIfRunning,它的返回值表示取消尝试是否成功(这仅仅告诉你它是否能够接收中断,而不是任务是否检测并处理了中断).当mayInterruptIfRunning为true,并且任务当前正在运行于一些线程中,那么这个线程是应该中断的.把这个参数设置成false意味着"如果还没有启动的话,不要运行这个任务",这应该用于那些不处理中断的任务.
//假想的Future..
Future futuretask = ...
//设置true并且任务运行在一些线程中,那么这个线程是应该中断的
// 设置为false,如果没启动就不要运行了
boolean cancel = futuretask.cancel(true);
//返回值cancel仅仅告诉你能否中断,而不是是否已经中断了
除非你知道线程的中断策略,否则你不应该中断线程,那么什么时候可以采用一个true作为参数调用cancel?任务执行线程是由标准的Executor实现创建的,它实现了一个中断策略,使得任务可以通过中断被取消,所以当它们在标准Executor中运行时,通过它们的Future来取消任务,这时设置mayInterruptIfRunning是安全的.
使用Future取消任务的好处(画重点):当尝试取消一个任务的时候,你不应该直接中断线程池,因为你不知道中断请求到达时,什么任务正在运行---只能通过任务的Future来做这件事情.这便是编写任务,让它视中断为取消请求的另一个理由:能够通过它们的Future被取消.
使用future取消任务:
public class InterruptTest {
private static final ExecutorService executor = Executors.newFixedThreadPool(10);
public static void timedRun(Runnable r,Long timeOut,TimeUnit timeUnit){
Future f = executor.submit(r);
try {
f.get(timeOut,timeUnit);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
//executor里面有中断策略,所以这个 操作时线程安全的.
f.cancel(true);
}
}
}
当Future.get抛出InterruptedException或TimeoutException时,如果你知道不再需要结果时,就可以调用Future.cancel来取消任务了.
处理不可中断阻塞
支持中断的可阻塞的库方法提前返回和抛出InterruptedException来实现对中断的响应,但是有些不支持中断的阻塞方法,例如同步Socket I/O或者等待获得内部锁而阻塞,那么中断除了能够设置线程的中断状态该以外,什么都不能改变.但是我们可以通过覆写线程的interrupt或Future.cancel方法来自定义取消策略.
我们以同步的Socket I/O为例,在服务器应用程序中,阻塞I/O最常见的形式是读取和写入Socket.InputStream和Output中的read和write方法都不响应中断,但是通过关闭底层的Socket,可以让read或write锁阻塞的线程抛出一个SocketException.
public class RenderThread extends Thread {
//final 安全发布
private final Socket socket;
private final InputStream inputStream;
public RenderThread(Socket socket) throws IOException {
this.socket = socket;
this.inputStream = socket.getInputStream();
}
//重写中断方法
@Override
public void interrupt(){
try {
//关闭socket停止不可阻塞的方法
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run(){
//拿到byte
byte[] bytes = new byte['a'];
while (true){
try {
/*
* 从输入流读取数据的下一个字节,如果这里阻塞住了,
* 通过interrupt方法中断
* */
int count = inputStream.read(bytes);
if(count< 0){
break;
}else{
//进行一些处理..
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
自定义Future取消不可中断的阻塞操作
这个例子是上个例子的升级版,之前说过用Future中断线程的方法是最简单明了的,可以将Callable或者Runnable转换成Future再取消,这是Java6中添加到ThreadPoolExecutor的新特性,当提交一个Callable给ExecutorService时,submit返回一个future,可以用Future来取消任务.线程池内部就是使用newTaskFor创建Future来代替任务.它返回一个RunnableFuture,这是一个接口,它扩展了Future和Runnable(并由FutureTask实现);
将Callable转换成future的源码:
/**
* @throws RejectedExecutionException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
return new FutureTask<T>(callable);
}
自定义的任务Future允许你覆写Future.cancel方法.自定义的取消代码可以实现日志或者收集取消的统计信息,并可以用来取消那些不响应中断的活动.
使用future更改上面的例子:
//类1
public interface CallableTask<T> extends Callable<T> {
//扩展的两个方法
void cancel();
RunnableFuture<T> newTask();
}
//类2
public class CancellingExecutor extends ThreadPoolExecutor {
@Override
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable){
//判断传入的callable是不是我们自定义的callable的实例
if(callable instanceof CallableTask){
return ((CallableTask<T>) callable).newTask();
}else{
return super.newTaskFor(callable);
}
}
//类3
public class SocketUsingTask<T> implements CallableTask<T> {
private Socket socket;
protected synchronized void setSocket(Socket socket) {
this.socket = socket;
}
@Override
public synchronized void cancel() {
try {
if (socket != null) {
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public RunnableFuture<T> newTask() {
return new FutureTask<T>(this){
@Override
public boolean cancel(boolean mayInterruptIfRunning){
try {
SocketUsingTask.this.cancel();
} finally {
//返回值告诉你是否可以被中断
return super.cancel(true);
}
}
};
}
}
解析:要通过future.cancel取消任务,当调用cancel方法的时候就要使socket关闭,这是类3实现的,使用类2的线程池可以返回我们自定义的future,而类1是个接口扩展类取消和封装的方法,类3是他的实现类.
停止基于线程的服务
线程池拥有它的工作者线程,如果需要中断这些线程,那么应该由线程池负责.
ExecutorService提供了shutdown和shutdownNow方法,其他线程持有的服务也应该提供类似的关闭机制.
对于线程持有的服务,只要服务的存在时间大于创建线程的方法存在的时间,那么就应该提供生命周期方法.
使用exec.shutdown()和exec.awaitTermination(TIMEOUT,UNIT)配合使用关闭线程服务.
awaitTermination方法:接收人timeout和TimeUnit两个参数,用于设定超时时间及单位。当等待超过设定时间时,会监测ExecutorService是否已经关闭,若关闭则返回true,否则返回false。一般情况下会和shutdown方法组合使用。
shutdownNow的局限性
当通过shutdownNow强行关闭一个ExecutorService时,它试图取消正在进行的任务,并返回那些已经提交、但并没有开始的任务的清单,这样,这些任务可以被日志记录,或者存起来等待进一步处理.
但是那些已经开始、却没有结束的任务,却没有办法被找出来.
保存被取消的任务,示例:
public class TrackingExecutor extends AbstractExecutorService {
private final ExecutorService exec;
private final Set<Runnable> tasksCancelledAtShutdown = Collections.synchronizedSet(new HashSet<Runnable>());
public TrackingExecutor(ExecutorService exec) {
this.exec = exec;
}
public ArrayList<Runnable> getCancelledTasks(){
if(!exec.isTerminated()){
throw new IllegalStateException();
}
//返回一个新的list,不会发布这个set,否则还得加锁,要不然有可见性问题
return new ArrayList<Runnable>(tasksCancelledAtShutdown);
}
@Override
public void execute(final Runnable command) {
exec.execute(new Runnable() {
@Override
public void run() {
try {
command.run();
} finally {
if(isShutdown() && Thread.currentThread().isInterrupted()){
//把任务放进取消任务的集合
tasksCancelledAtShutdown.add(command);
}
}
}
});
}
//省略若干方法
}
TrackingExecutor存在不可避免的竞争条件,使它产生假阳性(false positive)现象: 识别出的被取消任务事实上可能已经结束.产生的原因是在任务执行完之后,线程池记录任务结束之前,线程池可能发生关闭.如果任务是幂等的(两次执行得到的结果与一次相同),那么这不会有什么问题.应用程序得到已被取消的任务必须注意这个风险.
处理反常的线程终止
异常终止的线程会"死掉",线程池在线程死掉以后,可能会用新的线程取代这个工作线程,保证不能正常运转的任务不会影响到后续任务的执行.
当线程失败的时候,应用程序可能看起来仍在工作,所以它的失败可能就会被忽略.幸运的是,我们有方法可以检测和终止线程从程序中"泄漏".
导致线程死亡的最主要的原因是RuntimeException.因为这些异常表明一个程序错误或者其他不可修复的错误,它们通常是不能被捕获的.它们不会顺着栈的调用传递,此时,默认的行为实在控制台打印追踪的信息,并终止线程.
任何代码都可以抛出RuntimeException.无论何时,当你调用另一个方法,你都要对他的行为保持怀疑,不要盲目地认为它一定会正常返回,或者一定会抛出在方法签名中声明的受检查的异常.当你调用的代码越不熟悉,你就越应该坚持对代码行为的怀疑
如果任务抛出了一个未检查的异常,它将允许线程终结,但是会首先通知框架:线程已经终结.然后,框架可能会用心的线程取代这个工作线程,也可能不这样做,因为线程池也许正在关闭,抑或当前已有足够多多线程,能够满足需要了.
ThreadPoolExecutor使用这项技术确保那些不能正常运转的任务不会影响到后续任务的执行,源码:
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {
w.lock();
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
为捕获异常的处理
线程的API提供了UncaughtExceptionHandler的工具,使你能够检测到线程因未捕获的异常引起的"死亡".这两个方案互为补充:合在一起,组成了对抗线程泄漏点强有力的保障.
当一个线程因为未捕获异常而退出时,JVM会把这个事件报告给应用程序提供的UncaughtExceptionHandler:如果处理器(handler)不存在,默认的行为是向Systeeem.err打印出站追踪信息.
实现UncaughtExceptionHandler接口
public class UEHlogger implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
Logger logger = Logger.getAnonymousLogger();
logger.log(Level.SEVERE,"Thread terminated with exception:"+t.getName());
}
}
在一个长时间运行的应用程序中,所有的线程都要给未捕获异常设置一个处理器,这个处理器至少要将异常信息记入日志中.
可以将刚刚自定义的UncaughtExceptionHandler作为参数(ThreadFachory)传递给线程池.标准线程池允许未捕获的任务异常去结束池线程,但是使用一个try-finally块来接收通知的话,当池线程被终结后,能够有新的线程取代它.如果没有非捕获异常的处理器,或者其他失败通知机制,任务会无声无息地失败,这会导致混乱.如果你想在任务因为异常而失败时获得通知,那么你应该采取一些特定任务的恢复行为,或者用Runnable与Callable把任务包装起来,这样就能够捕获异常,或者覆写ThreadPoolExecutor的afterExecute钩子(回调)方法.
令人有些混淆的是,只有通过Execute提交的任务,才能将它抛出的异常送交给未捕获异常的处理器;而通过submit提交的任务,抛出的任何异常,无论是否为受检查的,都被认为是任务返回状态的一部分.如果有submit提交的任务以异常作为终结,这个异常会被Future.get重抛出,包装在ExecutionException中.
关闭钩子
关闭钩子会在应用程序关闭的时候,执行一些自定义的操作(比如清理临时文件).
想使用关闭钩子必须先要注册钩子,注册:
public void hookTest(){
Runtime.getRuntime().addShutdownHook(new Thread(){
@Override
public void run(){
System.out.println("退出时执行了关闭钩子函数");
}
});
}
这样在应用程序关闭的时候就会执行"退出时执行了关闭钩子函数".
可以执行System.exit来退出应用程序:
public void exit(){
//参数0代表正常退出
System.exit(0);
}
注意必须先调用上面注册关闭钩子的方法注册钩子,才能在关闭的时候使用.
要使用钩子函数必须保证关闭钩子是线程安全的:它们在访问共享数据时必须使用同步,并应该小心地避免死锁.
使用关闭钩子时对所有服务使用唯一的关闭钩子,因为所有钩子都是并发执行的不能保证顺序,如果关闭一个服务依赖于另一个服务的时候这会引发问题.让钩子调用一些列关闭行为,而不是每个服务使用一个可以避免这个问题.
总结
任务、线程、服务以及应用程序在生命周期结束时的问题,可能会导致它们引入复杂的设计和实现.Java没有提供具有明显优势的机制来取消活动或者终结线程.它提供了协作的中断机制,能够用来帮助取消,但是这将取决你如何构建取消的协议,并是否能一致地使用该协议.使用FutureTask和Executor框架可以简化构建可取消的任务和服务.
下篇, 自定义构建线程池.
拜.
喜欢我的博客就请点赞+【关注】一波