被忽略的InterruptedException
我们在写
Java
程序的时候,经常会调用Thread.sleep()
来让线程休眠一会。而后我需要用try catch
将其包裹起来,如果不这样做程序不会通过编译,因为sleep
方法会抛出受检查类型的异常(checked exception)InterruptedException
。但是很多的人做法是捕捉异常后不会做任何处理,或者说只会打印异常堆栈信息。然而这样做是否合适,sleep
方法又为何会抛出InterruptedException
?
1 | try { |
如何停止一个线程 ?
首先我们先探讨一下,在Java
中如何让一个Thread
停止运行。
stop 方法
Java 曾为Thread
提供过stop
方法,用来让一个Thread
停止运行,不过已经被标记为Deprecated,理由是stop
是个很危险的方法。
1 | class Task extends Thread{ |
上面的代码Task
为完成doSomeThing
的任务,会申请一些资源,完成后会将这些资源释放。假设我们为Task
创建一个实列,然后我我们调用start
方法,使线程运行,之后在某个时间段我们调用stop
方法让线程终止执行。假如这时候线程线程正在执行doSomething
的某条指令,这时候线程突然终止,就会带来一个问题前面申请的资源没有释放,线程就结束了。
那我们有没有办法监听到线程被
stop
,然后释放资源呢 ?
ThreadDeath
stop
方法停止线程的方式是抛出一个ThreadDeath
错误,这样我们可以通过捕捉ThreadDeath
来释放资源。
1 | class Task extends Thread { |
看起来我们的问题解决了?
一个重要的问题是我们的并不知道线程究竟是在哪一条指令停止了运行,我的善后工作可能也只是放弃本次线程做执行的任务,并恢复至线程执行之前的状态。
stop 方法对监视器的影响
现在假设有一个多个线程协作的场景,Task1
的执行依赖于Task0
, Task0
和Task1
持有同一个monitor
。我们依次调用Task0
和Task1
的start()
方法让它们执行,Task0
执行过程中,Task1
处于阻塞状态,直到Task0
执行完毕释放了锁Task1
才开始执行。
1 | class Task0 extends Thread{ |
通过stop()方法停止一个线程,会释放其所有的监视器如果在Task0
执行过程中,我们调用了其stop
方法,让其停止运行。这时Task0
并没有完成其工作,然后释放了锁。之后Task1
就获取的执行权,我们上面提到Task1
的执行要依赖于Task0
,然而 Task0
并未执行完成。Task1
所执行时使用的时Task0
产生的错误的不完整的数据。
这时候我即使捕捉了ThreadDeath
错误也很难解决问题,锁总归是要被释放的,我们不能保证其他竞争该monitor
的其他线程安全的执行,然而,对此我几乎无能为力。
所以
stop
方法停止一个线程是一种粗暴的方式,可能会给我们的程序带来严重的后果。就像你正在用电脑工作,这时候有人突然把电源给你拔掉了。我们如何优雅安全的终止一个线程呢 ?
interrupt 方法
前面讲到使用stop
方法粗暴的停止一个线程是很不安全的,已经被标记为Deprecated,替代的方法就是interrupt
。每一个线程都会维护一个中断状态,通过调用线程interrupt
方法的方式将线程标记为中断状态,让线程通过中断状态自己终止执行,线程何时停止运行、在何处停止运行、甚至是否停止运行都有线程本身自己决定。
interrupt 相关的方法
1 | void interrupt() //将一个线程标记为中断 状态 |
interrupt()
将一个线程标记为中断状态isInterrupted()
判断线程是否处于中断状态interrupted()
是一个静态方法,会重置当前线程的中断状态
1 | class Task extends Thread { |
上面代码Task
要执行的是一个周期任务,我们可以将每一次任务的间隔作为安全点,判断线程是否被中断。Task
在执行的过程,在其他线程被调用interrupt
方法,线程Task
通过 isInterrupted()检测是否被中断,若是中断状态后停止while
循环退出run
方法。
上面的例子在执行周期任务,在执行短暂的非周期任务时,可能我并不需要关注中断状态
interrupt 与阻塞方法
阻塞方法何时执行完成我们一般不可控,例如wait
方法何时执行完成依赖于有没有被其他线程唤醒。为了以保证线程响应中断指令,如果被调用了interrupt
方法的线程处于休眠阻塞状态,该线程会被立刻切换至可执行状态,不会等待其阻塞方法执行完成。实现方式就是阻塞方法抛出 InterruptedException,一个阻塞方法抛出一个InterruptedException
,代表阻塞休眠任务并未完成,当前线程被中断。 (IO相关操作不允许被强制中断,所以与IO相关的阻塞方法不会抛出InterruptedException)
1 | try { |
InterruptedException 抛出后会清除中断标记
1 | class Task extends Thread { |
上面的代码线程 Task 启动后,在Thread.sleep(5000)
的执行过程中我们调用了interrupt
方法,但是由于我们捕捉了 sleep
方法的InterruptedException
异常,于是线程的中断状态又被清除了。此时while (!isInterrupted())
将永远是 true
。所以为了保证后面代码能够正确响应线程的中断状态,我们在捕捉完InterruptedException
异常做完处理之后要手动的将线程设为中断状态。
1 | class Task extends Thread { |
遇到 InterruptedException 的处理方式
method0
处理完处理完InterruptedException
,重新将线程设为中断状态method1
直接抛出method2
处理完InterruptedException
,重新将其抛出
总之不要生吞 InterruptedException,要让后续代码能后正确的响应线程的中断状态。
1 | class Task extends Thread { |
线程停止的正确方式
由于我们不能保证我引用第三方代码能够正确的处理中断,所以并不能完全依靠isInterrupted
方法读取中断状态来终止线程。比较保险的方式我们手动添加一个状态标志位
1 | class Task extends Thread { |
记住,不要忽略InterruptedException !