小节
为什么需要取消和关闭: 有时候我们希望在任务或线程自然结束之前就停止它们,可能因为用户取消了操作,或者应用程序需要快速关闭.
取消和关闭的好处: 不会浪费资源执行一些没用的操作、保证程序的正常退出.
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来保存中断状态.
如果你的任务代码响应中断,不要使用自定义的退出标识,使用线程的中断作为你的取消机制,这样在代码阻塞的时候你依然可以退出任务.示例:<
