7取消与关闭

概念

  • 中断(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;

/**
 * Time:	2018/8/3 13:52
* Auth: weicm
*
* Desp: 在外部线程中安排中断
*
* 优点:
*
    *
  • 能从任务中抛出未检查异常,异常会被timedRun的调用者捕获
  • *
*
* 缺点:
*
    *
  • 在中断线程时,不了解执行任务的线程的中断策略,因为timedRun可能在任何线程中运行
  • *
  • 如果任务不响应中断,那么timedRun会在任务结束时才返回,此时可能已经超过了指定时限,这会对调用者带来负面影响
  • *
*/ 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(); } } /** * Time: 2018/8/3 14:20
* Auth: weicm
*
* Desp: 在制定时限内运行任务,超过时限则取消任务
* * @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;

/**
 * Time:	2018/8/3 15:36
* Auth: weicm
*
* Desp: 在专门的线程中中断任务
*
* 优点:
*
    *
  • 解决了TimedRun1的所有缺点
  • *
*
* 缺点:
*
    *
  • 由于join的不足,无法知道任务是因为线程正常退出而返回还是因为join超时而返回
  • *
*/ 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(); } } /** * Time: 2018/8/3 14:20
* Auth: weicm
*
* Desp: 在制定时限内运行任务,超过时限则取消任务
* * @param task 任务 * @param timeout 时限数量 * @param unit 时限单位 */ static void timedRun(Runnable task, long timeout, TimeUnit unit) throws Throwable { /** * Time: 2018/8/3 14:42
* Auth: weicm
*
* Desp: 装执行线程的中断策略
*/ class ThrowableTask implements Runnable { private volatile Throwable e; @Override public void run() { try { task.run(); } catch (Throwable e) { //中断策略: 录任务的运行时异常,以便稍后重新抛出该异常,并结束执行线程 this.e = e; } } /** * Time: 2018/8/3 15:33
* Auth: weicm
*
* Desp: 重新抛出目标任务运行过程中可能发生的异常
* * @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.*;

/**
 * Time:	2018/8/3 16:38
* Auth: weicm
*
* Desp: 通过Future来取消任务
*
* 优点:
*
    *
  • 解决TimedRun2的缺点,可以区分任务是如何结束的
  • *
*
* 关于Futrue.cancel: 针对任务的三种状态
*
    *
  • 等待状态:此时不管参数传入的是true还是false,任务都会被标记为取消,任务依然保存在队列中,但当轮询到此任务时会直接跳过
  • *
  • 运行状态:此时参数传入true会中断正在执行的任务;传入false则不会中断任务,而是让任务继续运行直到结束
  • *
  • 完成状态:此时不管参数传入的是true还是false,cancel都不起作用,因为任务已经完成了
  • *
*/ 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(); } } /** * Time: 2018/8/3 14:20
* Auth: weicm
*
* Desp: 在制定时限内运行任务,超过时限则取消任务
* * @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方法,该方法允许在等待一个锁的同时仍能响应中断。

你可能感兴趣的:(7取消与关闭)