7取消与关闭
2018-11-02 本文已影响0人
WFitz
概念
- 中断(Interruption),是一种协作机制,能够使一个线程终止另一个线程的当前工作。
- 中断只适合用于取消操作,如果用在取消之外的其他操作则是不合适的;中断是实现取消的最合理方式。
中断是必要的
- 我们很少希望某个任务、线程或服务立即停止,因为这样会使共享的数据结构处于不一致的状态;而使用中断这种协作机制,当需要停止时,任务首先会清除当前正在执行的工作,然后再结束,因为任务本身的代码比发出请求的代码更清楚如何执行清除工作。
行为好坏的程序的区别
- 行为良好的程序能很完善地处理失败、关闭和取消等情况。
取消任务概念
- 外部代码能在某个任务正常完成之前将其置为“完成”状态
任务取消原因
- 用户请求取消(例如:用户点击取消按钮)
- 有时间限制的操作(例如:任务执行超时,则取消任务)
- 应用程序事件(例如:一个任务的完成事件,导致其他任务取消)
- 错误(例如:一个任务发生错误,导致其他任务取消)
- 关闭(例如:程序运行结束,取消长运行的任务)
任务取消策略
- How:外部代码如何请求取消任务
- When:任务在何时检查外部代码是否已经请求了取消
- What:任务取消时应该执行哪些操作
线程中断策略
- 中断策略规定线程如何解释某个中断请求
- 最合理的中断策略是某种线程级或服务级取消操作:
- 尽快退出或抛出InterruptException异常
- 在必要的时候进行清理(可以在线程中断后进行清理工作,例如shutdown方法内清理)
- 通知某个所有者该线程已经退出(标记被中断线程的状态为中断)
- 其他常用中断策略:停止服务、重新开始服务,对于包含这些非标准中断策略的线程或线程池,只能应用于知道这些策略的任务中。
注:正如任务中应该包含取消策略,线程同样应该包含中断策略
Thread中断相关操作
- interrupt: 用于中断目标线程
- isInterrupted: 用于检测目标线程是否会中断
- Thread.interrupted: 用于清除当前线程的中断状态,并返回之前的中断状态,这是清除中断状态的唯一方法
注1:如果目标线程已经结束,则 isInterrupted 始终返回false!
注2:调用interrupt并不意味着立即停止目标线程正在执行的任务,而只是传递了请求中断信息!
支持中断的阻塞库方法
- Thread.sleep
- Thread.join
- Object.wait
注:它们响应中断的操作包括:清除中断状态,抛出InterruptedException。也就是说,收到InterruptedException时,中断状态已经为 false !
中断请求的接收者
- 任务和线程都是中断请求的接收者,一个中断请求到达时,意味着需要“取消任务”和“关闭工作者线程”
- 任务不会在自己拥有的线程中运行(任务只是任务,不是线程),任务是非线程所有者,在处理中断请求时应该小心的保存中断状态,这样线程拥有者才能对中断做出响应,即使其他后续“非拥有者”也可以做出响应。
任务响应中断的两种方式
- 传递异常InterruptedException
- 恢复中断状态(调用Thread.interrupt方法)
注1:任务不应该对执行该任务的线程的中断策略做出任何假设,除非该任务被专门设计为服务中运行,并且在这些服务中包含特定的中断策略!
注2:只有实现了线程中断策略的代码才可以屏蔽中断请求,在常规的任务和库代码中都不应该屏蔽中断请求!
如何中断线程
- 线程只能由其所有者中断,所有者可以将线程的中断策略信息封装到某个合适的取消机制中,里如果关闭(shutdown)方法。
任务对中断的响应
- 支持取消且调用了中断阻塞方法的任务,应该尽快取消任务,恢复中断状态
- 不支持取消但在循环中调用中断阻塞方法的任务,应该在本地保存中断状态,并在任务返回前恢复中断状态,而不是在捕获中断时就恢复,那样容易引起死循环
- 不调用中断阻塞方法的任务,在循环中轮询当前线程的中断状态来响应中断
Java中断机制的优点
- Java中断为非抢占式中断,通过推迟中断请求的处理,开发人员能指定灵活的终端策略,从而使应用程序在响应性和建壮性之间实现合理的平衡。
标准取消操作示例
package cn.weicm.cancel;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
/**
* <b>Time:	2018/8/3 13:52</b><br/>
* <b>Auth:	weicm</b><br/>
* <br/>
* <b>Desp:	在外部线程中安排中断</b><br/>
* <br/>
* <b>优点:</b><br/>
* <ul>
* <li>能从任务中抛出未检查异常,异常会被timedRun的调用者捕获</li>
* </ul>
* <br/>
* <b>缺点:</b><br/>
* <ul>
* <li>在中断线程时,不了解执行任务的线程的中断策略,因为timedRun可能在任何线程中运行</li>
* <li>如果任务不响应中断,那么timedRun会在任务结束时才返回,此时可能已经超过了指定时限,这会对调用者带来负面影响</li>
* </ul>
*/
public class TimedRun1 {
private static final ScheduledExecutorService ses = Executors.newScheduledThreadPool(1);
public static void main(String[] args) {
try {
timedRun(() -> {
System.out.println("Task start ...");
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
System.out.println("Task is canceled!");
//假设任务不响应中断,即不抛出InterruptedException,也不通过Thread.interrupt()恢复中断状态,而是直接退出
return;
}
System.out.println("Task end ...");
}, 1, TimeUnit.SECONDS);
} finally {
ses.shutdown();
}
}
/**
* <b>Time:	2018/8/3 14:20</b><br/>
* <b>Auth:	weicm</b><br/>
* <br/>
* <b>Desp:	在制定时限内运行任务,超过时限则取消任务</b><br/>
*
* @param task 任务
* @param timeout 时限数量
* @param unit 时限单位
*/
static void timedRun(Runnable task, long timeout, TimeUnit unit) {
//获取目标任务所在线程,此时该线程可能是任意线程
Thread currentThread = Thread.currentThread();
//启动中断任务
ScheduledFuture<?> future = ses.schedule(() -> {
currentThread.interrupt();
}, timeout, unit);
task.run();
//任务结束后,取消掉中断任务,因为此时目标任务已经结束,中断任务已经没有存在的意义了
future.cancel(true);
}
}
package cn.weicm.cancel;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
/**
* <b>Time:	2018/8/3 15:36</b><br/>
* <b>Auth:	weicm</b><br/>
* <br/>
* <b>Desp:	在专门的线程中中断任务</b><br/>
* </br>
* <b>优点:	</b><br/>
* <ul>
* <li>解决了TimedRun1的所有缺点</li>
* </ul>
* </br>
* <b>缺点:	</b><br/>
* <ul>
* <li>由于join的不足,无法知道任务是因为线程正常退出而返回还是因为join超时而返回</li>
* </ul>
*/
public class TimedRun2 {
private static final ScheduledExecutorService ses = Executors.newScheduledThreadPool(1);
public static void main(String[] args) throws Throwable {
try {
timedRun(() -> {
System.out.println("Task start ...");
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
System.out.println("Task is canceled!");
//假设任务不响应中断,即不抛出InterruptedException,也不通过Thread.interrupt()恢复中断状态,而是直接退出
return;
}
System.out.println("Task end ...");
}, 1, TimeUnit.SECONDS);
} finally {
ses.shutdown();
}
}
/**
* <b>Time:	2018/8/3 14:20</b><br/>
* <b>Auth:	weicm</b><br/>
* <br/>
* <b>Desp:	在制定时限内运行任务,超过时限则取消任务</b><br/>
*
* @param task 任务
* @param timeout 时限数量
* @param unit 时限单位
*/
static void timedRun(Runnable task, long timeout, TimeUnit unit) throws Throwable {
/**
* <b>Time:	2018/8/3 14:42</b><br/>
* <b>Auth:	weicm</b><br/>
* <br/>
* <b>Desp:	装执行线程的中断策略</b><br/>
*/
class ThrowableTask implements Runnable {
private volatile Throwable e;
@Override
public void run() {
try {
task.run();
} catch (Throwable e) {
//中断策略: 录任务的运行时异常,以便稍后重新抛出该异常,并结束执行线程
this.e = e;
}
}
/**
* <b>Time:	2018/8/3 15:33</b><br/>
* <b>Auth:	weicm</b><br/>
* <br/>
* <b>Desp:	重新抛出目标任务运行过程中可能发生的异常</b><br/>
*
* @throws Throwable
*/
public void rethrow() throws Throwable {
if (null != e)
throw e;
}
}
//将目标任务运行在明确中断策略的执行线程里
ThrowableTask t = new ThrowableTask();
Thread taskThread = new Thread(t);
taskThread.start();
//启动中断任务
ScheduledFuture<?> future = ses.schedule(() -> {
taskThread.interrupt();
}, timeout, unit);
taskThread.join(unit.toMillis(timeout));
//任务结束后,取消掉中断任务,因为此时目标任务已经结束,中断任务已经没有存在的意义了
future.cancel(true);
//重新抛出任务执行过程中发生的异常
t.rethrow();
}
}
package cn.weicm.cancel;
import java.util.concurrent.*;
/**
* <b>Time:	2018/8/3 16:38</b><br/>
* <b>Auth:	weicm</b><br/>
* <br/>
* <b>Desp:	通过Future来取消任务</b><br/>
* </br>
* <b>优点:	</b><br/>
* <ul>
* <li>解决TimedRun2的缺点,可以区分任务是如何结束的</li>
* </ul>
* </br>
* <b>关于Futrue.cancel:	针对任务的三种状态</b><br/>
* <ul>
* <li>等待状态:此时不管参数传入的是true还是false,任务都会被标记为取消,任务依然保存在队列中,但当轮询到此任务时会直接跳过</li>
* <li>运行状态:此时参数传入true会中断正在执行的任务;传入false则不会中断任务,而是让任务继续运行直到结束</li>
* <li>完成状态:此时不管参数传入的是true还是false,cancel都不起作用,因为任务已经完成了</li>
* </ul>
*/
public class TimedRun3 {
private static final ExecutorService es = Executors.newSingleThreadExecutor();
public static void main(String[] args) throws Exception{
try {
timedRun(() -> {
System.out.println("Task start ...");
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
System.out.println("Task is canceled!");
//假设任务不响应中断,即不抛出InterruptedException,也不通过Thread.interrupt()恢复中断状态,而是直接退出
return;
}
System.out.println("Task end ...");
}, 1, TimeUnit.SECONDS);
} finally {
es.shutdown();
}
}
/**
* <b>Time:	2018/8/3 14:20</b><br/>
* <b>Auth:	weicm</b><br/>
* <br/>
* <b>Desp:	在制定时限内运行任务,超过时限则取消任务</b><br/>
*
* @param task 任务
* @param timeout 时限数量
* @param unit 时限单位
*/
static void timedRun(Runnable task, long timeout, TimeUnit unit) throws Exception {
Future<?> future = es.submit(task);
try {
future.get(timeout, unit);
} catch (InterruptedException e) {
//当前线程被中断,恢复中断状态以传递中断信息
Thread.currentThread().interrupt();
} catch (ExecutionException e) {
//目标任务执行过程发生运行时异常,直接抛出
throw e;
} catch (TimeoutException e) {
//目标任务执行超时,接下来取消任务,因为已经不需要结果了
} finally {
//如果任务已经运行完,则取消操作不产生任何效果;如果任务由与异常而终止,不管什么异常,则取消任务,因为已经不需要结果了
//取消那些不在需要结果的任务是一种良好的习惯!
future.cancel(true);
}
}
}
不可中断的阻塞
- Thread.interrupt对于执行不可中断的操作而阻塞的线程,只能设置线程的中断状态,除此之外没有其他任何作用;但可以使用类似于中断的手段来停止这些线程,但是必须知道线程阻塞的原因。
常见不可中断的阻塞操作
- Java.io包中的同步Socket I/O: InputStream.read和OutputStream.write,可以关闭套接字(Socket.close)使他们抛出SocketException。
- Java.io包中的同步I/O: 当中断一个正在InterruptibleChannel上等待的线程时,将抛出ClosedByInterruptException并关闭链路,这还会使得其他在该链路上阻塞的线程同样抛出ClosedByInterruptException。当关闭一个InterruptibleChannel时,将导致所有在链路操作上阻塞的线程都抛出AsynchronousCloseException。
- Selector的异步I/O: 如果一个线程在调用Selector.select方法时阻塞了,那么调用close或wakeup方法会使线程抛出ClosedSelectorException并提前返回。
- 获取某个锁: 如果一个线程由与等待某个内置锁而阻塞,那么将无法响应中断,因为线程认为它肯定会获得锁,所以将不会理会中断请求。但是Lock类中提供了lockInterruptiblely方法,该方法允许在等待一个锁的同时仍能响应中断。