1問題描述
在應用軟件的開發中,經常會遇到這樣的一種需求:需要實現一個方法來執行某種任務,而這個方法的執行時間不能超過指定值,如果超時,則調用者不管這個方法將來是否可能執行成功,都要中斷它的執行,或者讓這個方法返回。這就是超時處理問題。
根據執行任務的方法是否異步,可以把問題從兩個方面分析:如果方法順序執行,則方法執行時整個程序的控制權在執行任務的方法中,方法調用者對於任務的超時無能為力,只能寄希望於執行任務的方法能夠在任務的每輪循環中判斷是否超時,以便隨時自己返回;如果任務方法異步執行,即執行任務的方法是另一個線程,則可以通過主線程和任務線程的線程間協作來實現任務線程的超時中斷處理。
2解決方案
根據上面對問題的分析,可以提出三種解決方案,一種同步的解決方案和兩種異步的解決方案。
2.1串行超時處理
串行超時處理是指程序只有一個線程,調用者調用任務方法,完全由執行任務的方法本身進行超時處理。
這種方案通常要求任務需要循環執行,每個循環內的計算較復雜,執行時間較長或者不確定。執行任務的方法在規划任務算法代碼的同時還要考慮超時的時候能夠退出。通常的代碼框架如下:
1 |
public void runTask(long timeout){ |
2 |
long beginTime=System.currentTimeMillis(); |
3 |
//任務執行准備 |
4 |
//如下為任務算法執行 |
5 |
while((System.currentTimeMillis()-beginTime<timeout)&&(任務自身的邏輯判斷)){ |
6 |
//執行循環體內的任務片段和算法 |
7 |
} |
8 |
} |
通過這種方案實現的任務超時處理最大的優點是方案簡單,因為不會引入新的線程,完全串行操作,但是這種方案也有兩大缺點:
1、代碼混亂,因為方法除了實現任務,還要考慮超時,違反了方法的職責單一原則
2、該方案無法處理因阻塞引起的超時情況
第二個缺點是這個方案的最大限制。如果循環體內出現諸如IO阻塞而引起的程序執行掛起,比如socket.accept(),或者inputstream.read(),這樣方法在因阻塞引起的超時發生后將不會返回,因為這時根本無法執行到下次循環的判斷條件處。
為了能夠處理阻塞超時的情況,只能借助異步多線程方式來完成。
2.2利用wait/notify實現異步超時處理
利用多線程機制實現任務方法的異步執行很簡單,只需要創建一個類實現Runnable接口,把任務放在重寫的run方法中,run方法即為處理任務的方法。在主線程中使用Thread類創建並啟動新任務線程即可。
但是,如果需要處理任務線程可能的超時情況,就需要wait/notify機制讓主線程和任務線程同步了。具體思路是:讓主線程在啟動任務線程之后進行帶超時參數的wait操作,如果任務線程超時,則wait不再等待,wait返回后主動中斷任務線程;如果在超時時間內任務線程執行完畢,則通過notify方法通知主線程,這樣主線程的wait方法也可以返回。
主線程代碼框架如下,假設任務線程為TaskThread:
01 |
public void caller(){ |
02 |
Object monitor=new Object(); |
03 |
TaskThread task=new TaskThread(); |
04 |
Thread thread=new Thread(task); |
05 |
//對task對象進行各種set操作以初始化任務 |
06 |
try{ |
07 |
synchronized(monitor){ |
08 |
thread.start(); |
09 |
while(線程沒有順利完成){ |
10 |
monitor.wait(timeout); |
11 |
} |
12 |
//線程順利結束,獲取並處理結果 |
13 |
} |
14 |
} |
15 |
catch(InterruptException e){ |
16 |
//等待已經被超時或者其他原因中斷,終止線程thread |
17 |
} |
18 |
finally{ |
19 |
//進行資源回收等收尾工作 |
20 |
} |
21 |
} |
任務線程TaskThread通過run方法實現任務,代碼框架如下:
01 |
@Override |
02 |
public void run(){ |
03 |
//各種任務執行准備 |
04 |
while((任務沒有被要求停止)&&(任務本身的各種判斷條件)){ |
05 |
//本次循環中的子任務處理 |
06 |
//包括各種可能的IO阻塞和掛起等待操作 |
07 |
} |
08 |
synchronized(monitor){ //此處的monitor引用必須主線程中的monitor對象 |
09 |
monitor.notify() //任務執行完畢,喚醒主線程的wait操作 |
10 |
} |
11 |
} |
這種方案可以處理因為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(); //實現Callable接口的任務線程類 |
03 |
ExecutorService exec=Executors.newFixedThreadPool(1); |
04 |
//對task對象進行各種set操作以初始化任務 |
05 |
Future<String> future=exec.submit(task); |
06 |
try{ |
07 |
return future.get(this.timeout, TimeUnit.MILLISECONDS); |
08 |
} |
09 |
finally{ |
10 |
if(任務線程沒有順利結束){ |
11 |
//終止線程task |
12 |
} |
13 |
exec.shutdownNow(); |
14 |
} |
15 |
} |
TaskThread由實現Runnable接口變成了實現Callable接口,call方法的代碼和run方法類似,只不過不需要notify方法的調用了。代碼框架如下:
1 |
@Override |
2 |
public String call() throws AnyException{ |
3 |
//各種任務執行准備 |
4 |
while((任務沒有被要求停止)&&(任務本身的各種判斷條件)){ |
5 |
//本次循環中的子任務處理 |
6 |
//包括各種可能的IO阻塞和掛起等待操作 |
7 |
} |
8 |
} |
通過比較可以看出,這種方案的代碼更為簡潔方便,雖然當任務超時時,終止任務線程的方法依然復雜,但是相比於前一種更簡潔,而且比第一種串行方案更具有通用性。
需要指出的是,如何停止一個線程是一個比較復雜而有一定技巧的工作(千萬別說用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.
