1问题描述
在应用软件的开发中,经常会遇到这样的一种需求:需要实现一个方法来执行某种任务,而这个方法的执行时间不能超过指定值,如果超时,则调用者不管这个方法将来是否可能执行成功,都要中断它的执行,或者让这个方法返回。这就是超时处理问题。
根据执行任务的方法是否异步,可以把问题从两个方面分析:如果方法顺序执行,则方法执行时整个程序的控制权在执行任务的方法中,方法调用者对于任务的超时无能为力,只能寄希望于执行任务的方法能够在任务的每轮循环中判断是否超时,以便随时自己返回;如果任务方法异步执行,即执行任务的方法是另一个线程,则可以通过主线程和任务线程的线程间协作来实现任务线程的超时中断处理。
2解决方案
根据上面对问题的分析,可以提出三种解决方案,一种同步的解决方案和两种异步的解决方案。
2.1串行超时处理
串行超时处理是指程序只有一个线程,调用者调用任务方法,完全由执行任务的方法本身进行超时处理。
这种方案通常要求任务需要循环执行,每个循环内的计算较复杂,执行时间较长或者不确定。执行任务的方法在规划任务算法代码的同时还要考虑超时的时候能够退出。通常的代码框架如下:
1 | public void runTask( long timeout){ |
2 | long beginTime=System.currentTimeMillis(); |
5 | while ((System.currentTimeMillis()-beginTime<timeout)&&(任务自身的逻辑判断)){ |
通过这种方案实现的任务超时处理最大的优点是方案简单,因为不会引入新的线程,完全串行操作,但是这种方案也有两大缺点:
1、代码混乱,因为方法除了实现任务,还要考虑超时,违反了方法的职责单一原则
2、该方案无法处理因阻塞引起的超时情况
第二个缺点是这个方案的最大限制。如果循环体内出现诸如IO阻塞而引起的程序执行挂起,比如socket.accept(),或者inputstream.read(),这样方法在因阻塞引起的超时发生后将不会返回,因为这时根本无法执行到下次循环的判断条件处。
为了能够处理阻塞超时的情况,只能借助异步多线程方式来完成。
2.2利用wait/notify实现异步超时处理
利用多线程机制实现任务方法的异步执行很简单,只需要创建一个类实现Runnable接口,把任务放在重写的run方法中,run方法即为处理任务的方法。在主线程中使用Thread类创建并启动新任务线程即可。
但是,如果需要处理任务线程可能的超时情况,就需要wait/notify机制让主线程和任务线程同步了。具体思路是:让主线程在启动任务线程之后进行带超时参数的wait操作,如果任务线程超时,则wait不再等待,wait返回后主动中断任务线程;如果在超时时间内任务线程执行完毕,则通过notify方法通知主线程,这样主线程的wait方法也可以返回。
主线程代码框架如下,假设任务线程为TaskThread:
02 | Object monitor= new Object(); |
03 | TaskThread task= new TaskThread(); |
04 | Thread thread= new Thread(task); |
07 | synchronized (monitor){ |
10 | monitor.wait(timeout); |
15 | catch (InterruptException e){ |
任务线程TaskThread通过run方法实现任务,代码框架如下:
04 | while ((任务没有被要求停止)&&(任务本身的各种判断条件)){ |
08 | synchronized (monitor){ |
这种方案可以处理因为IO或其他原因阻塞而引起任务执行超时的情况,当主线程因为wait超时抛出异常时,可以中断任务线程。但是需要注意的是,调用thread.stop()停止线程这种简单暴力的方法是不提倡使用的,而要用其他方法停止线程,而且停止一个处于阻塞状态的线程尤其复杂。
这种方案也存在如下缺点:
1、使用线程间同步操作,增加代码复杂度
2、Runnable接口的run方法没用返回值,不允许抛出异常,不便于任务完后了结果的返回
3、终止未完成的任务线程操作复杂
2.3利用Callable接口实现异步超时处理
为了去掉线程间的同步操作,以及能够让任务线程方法有返回值和抛出异常,可以使用Callable接口来替代Runnable接口,相应的主线程也需要相应变动。
Callable接口位于java.utils.concurrent包中,其抽象方法call()有返回值,可以抛出异常。调用Callable接口需要ExecutorService接口实例,而获取call方法的返回值需要Future接口实例。
主线程代码框架如下:
01 | public void caller() throws InterruptedExceptio,TimeoutExceptio,ExecutorException{ |
02 | TaskThread task= new TaskThread(); |
03 | ExecutorService exec=Executors.newFixedThreadPool( 1 ); |
05 | Future<String> future=exec.submit(task); |
07 | return future.get( this .timeout, TimeUnit.MILLISECONDS); |
TaskThread由实现Runnable接口变成了实现Callable接口,call方法的代码和run方法类似,只不过不需要notify方法的调用了。代码框架如下:
2 | public String call() throws AnyException{ |
4 | while ((任务没有被要求停止)&&(任务本身的各种判断条件)){ |
通过比较可以看出,这种方案的代码更为简洁方便,虽然当任务超时时,终止任务线程的方法依然复杂,但是相比于前一种更简洁,而且比第一种串行方案更具有通用性。
需要指出的是,如何停止一个线程是一个比较复杂而有一定技巧的工作(千万别说用Thread stop方法),因此并没有在本文中论述具体方法,这方面的知识可以参考资料[1,2],也可以参考我写的专门论述如何停止线程的文章http://blog.csdn.net/mikeszhang/article/details/8751355。
3参考资料
[1] Why Are Thread.stop, Thread.suspend, Thread.resume and Runtime.runFinalizersOnExit Deprecated?http://docs.oracle.com/javase/1.5.0/docs/guide/misc/threadPrimitiveDeprecation.html.
[2] How to Stop a Thread or a Task.http://forward.com.au/javaProgramming/HowToStopAThread.html.
[3] JavaTM Platform Standard Edition 6 API. http://www.ostools.net/apidocs/apidoc?api=jdk-zh.
[4] Bruce Eckel. Thinking in Java, 4th Edition. Prentice Hall, 2006.
http://my.oschina.net/u/1010788/blog/119361