Java——线程的中断
线程的中断1、取消/关闭的场景2、取消/关闭的机制3、线程对中断的反应3.1、Runnable3.2、Waiting/Timed_Waiting3.3、Blocked3.4、New/Terminate4、如何正确地取消/关闭线程1、取消/关闭的场景我们知道通过线程的start方法启动一个线程后线程开始执行run方法run方法运行结束后线程退出那为什么还需要结束一个线程呢有多种情况比如很多线程的运行模式是死循环比如在生产者/消费者模式中消费者主体就是一个死循环它不停地从队列中接受任务执行任务在停止程序时我们需要一种“优雅”的方法以关闭该线程。在一些图形用户界面程序中线程是用户启动的完成一些任务比如从远程服务器上下载一个文件在下载过程中用户可能会希望取消该任务。在一些场景中比如从第三方服务器查询一个结果我们希望在限定的时间内得到结果如果得不到我们会希望取消该任务。有时我们会启动多个线程做同一件事比如类似抢火车票我们可能会让多个好友帮忙从多个渠道买火车票只要有一个渠道买到了我们会通知取消其他渠道。2、取消/关闭的机制Java的Thread类定义了如下方法publicfinalvoidstop()这个方法看上去就可以停止线程但这个方法被标记为了过时简单地说我们不应该使用它可以忽略它。在Java中停止一个线程的主要机制是中断中断并不是强迫终止一个线程它是一种协作机制是给线程传递一个取消信号但是由线程来决定如何以及何时退出。Thread类定义了如下关于中断的方法publicbooleanisInterrupted()publicvoidinterrupt()publicstaticbooleaninterrupted()这三个方法名字类似比较容易混淆我们解释一下。isInterrupted()和interrupt()是实例方法调用它们需要通过线程对象interrupted()是静态方法实际会调用Thread. currentThread()操作当前线程。每个线程都有一个标志位表示该线程是否被中断了。isInterrupted返回对应线程的中断标志位是否为true。interrupted返回当前线程的中断标志位是否为true但它还有一个重要的副作用就是清空中断标志位也就是说连续两次调用interrupted()第一次返回的结果为true第二次一般就是false除非同时又发生了一次中断。interrupt表示中断对应的线程。中断具体意味着什么呢下面我们进一步来说明。3、线程对中断的反应interrupt()对线程的影响与线程的状态和在进行的IO操作有关。我们主要考虑线程的状态IO操作的影响和具体IO以及操作系统有关我们就不讨论了。线程状态有RUNNABLE线程在运行或具备运行条件只是在等待操作系统调度。WAITING/TIMED_WAITING线程在等待某个条件或超时。BLOCKED线程在等待锁试图进入同步块。NEW/TERMINATED线程还未启动或已结束。3.1、Runnable如果线程在运行中且没有执行IO操作interrupt()只是会设置线程的中断标志位没有任何其他作用。线程应该在运行过程中合适的位置检查中断标志位比如如果主体代码是一个循环可以在循环开始处进行检查如下所示publicclassInterruptRunnableDemoextendsThread{Overridepublicvoidrun(){while(!Thread.currentThread().isInterrupted()){//…单次循环代码}System.out.println(done );}//其他代码}3.2、Waiting/Timed_Waiting线程调用join/wait/sleep方法会进入WAITING或TIMED_WAITING状态在这些状态时对线程对象调用interrupt()会使得该线程抛出InterruptedException。需要注意的是抛出异常后中断标志位会被清空而不是被设置。比如执行如下代码ThreadtnewThread(){Overridepublicvoidrun(){try{Thread.sleep(1000);}catch(InterruptedExceptione){System.out.println(isInterrupted());}}};t.start();try{Thread.sleep(100);}catch(InterruptedExceptione){}t.interrupt();程序的输出为false。InterruptedException是一个受检异常线程必须进行处理。我们在异常处理中介绍过处理异常的基本思路是如果知道怎么处理就进行处理如果不知道就应该向上传递通常情况下不应该捕获异常然后忽略。捕获到InterruptedException通常表示希望结束该线程线程大致有两种处理方式向上传递该异常这使得该方法也变成了一个可中断的方法需要调用者进行处理有些情况不能向上传递异常比如Thread的run方法它的声明是固定的不能抛出任何受检异常这时应该捕获异常进行合适的清理操作清理后一般应该调用Thread的interrupt方法设置中断标志位使得其他代码有办法知道它发生了中断。第一种方式的示例代码如下publicvoidinterruptibleMethod()throwsInterruptedException{//…包含wait, join 或 sleep 方法Thread.sleep(1000);}第二种方式的示例代码如下publicclassInterruptWaitingDemoextendsThread{Overridepublicvoidrun(){while(!Thread.currentThread().isInterrupted()){try{//模拟任务代码Thread.sleep(2000);}catch(InterruptedExceptione){//...清理操作//重设中断标志位Thread.currentThread().interrupt();}}System.out.println(isInterrupted());}publicstaticvoidmain(String[]args){InterruptWaitingDemodemonewInterruptWaitingDemo();demo.start();demo.interrupt();}}3.3、Blocked如果线程在等待锁对线程对象调用interrupt()只是会设置线程的中断标志位线程依然会处于BLOCKED状态也就是说interrupt()并不能使一个在等待锁的线程真正“中断”。我们看段代码publicclassInterruptSynchronizedDemo{privatestaticObjectlocknewObject();privatestaticclassAextendsThread{Overridepublicvoidrun(){synchronized(lock){while(!Thread.currentThread().isInterrupted()){}}System.out.println(exit);}}publicstaticvoidtest()throwsInterruptedException{synchronized(lock){AanewA();a.start();Thread.sleep(1000);a.interrupt();a.join();}}publicstaticvoidmain(String[]args)throwsInterruptedException{test();}}test方法在持有锁lock的情况下启动线程a而线程a也去尝试获得锁lock所以会进入锁等待队列随后test调用线程a的interrupt方法并调用join等待线程线程a结束线程a会结束吗不会interrupt方法只会设置线程的中断标志而并不会使它从锁等待队列中出来。在使用synchronized关键字获取锁的过程中不响应中断请求这是synchronized的局限性。如果这对程序是一个问题应该使用显式锁。3.4、New/Terminate如果线程尚未启动NEW或者已经结束TERMINATED则调用interrupt()对它没有任何效果中断标志位也不会被设置。4、如何正确地取消/关闭线程interrupt方法不一定会真正“中断”线程它只是一种协作机制如果不明白线程在做什么不应该贸然地调用线程的interrupt方法以为这样就能取消线程。对于以线程提供服务的程序模块而言它应该封装取消/关闭操作提供单独的取消/关闭方法给调用者外部调用者应该调用这些方法而不是直接调用interrupt。Java并发库的一些代码就提供了单独的取消/关闭方法比如Future接口提供了如下方法以取消任务booleancancel(booleanmayInterruptIfRunning);再如ExecutorService提供了如下两个关闭方法voidshutdown();ListRunnableshutdownNow();