在用Java編寫並發程式時,往往會碰到某個線程因計算量大或因阻塞而一直處於無響應的情況,我們可能會等的不耐煩(也可能是不想讓它佔用太多資源)想及時終止掉它,那就需要用到任務逾時結束的技巧了。在剛接觸到多線程時,我本以為API會提供這樣一個多線程類:Thread(Runnable r, long timeout) ,第二個參數用來設定逾時時間,可事實並非如此。因為這樣的類不具有通用性,物件導向設計語言的目標是達到更進階的抽象,所以系統只提供了更廣泛的定時類,及其他一些類方法。這就需要我們藉助這些工具來達到任務逾時結束的目的。話不多說,直入正題。(PS:由於作者水平有限,這些方法只是給大家提供幾個思路,可能並不是教科書式的標準案例) 方法一:使用Thread.join(long million)
(先講一下本人對join方法的理解,已理解此方法的可以略過)join方法可以這樣理解,在理解它之前,先解釋另一個常識,即當前線程(後面稱為目標線程,因為它是我們想使其逾時結束的目標任務)的建立及start的調用,一定是在另一個線程中進行的(最起碼是main線程,也可以是不同於main線程的其他線程),這裡我們假設為main線程,並且稱之為依賴線程,因為目標線程的建立是在他裡面執行的。介紹完這些常識就可以進一步解釋了,join的字面意思是,使目標線程加入到依賴線程中去,也可以理解為在依賴線程中等待目標線程一直執行直至結束(如果沒有設定逾時參數的話)。設定了逾時參數(假設為5秒)就會這樣執行,在依賴線程中調用了join之後,相當於告訴依賴線程,現在我要插入到你的線程中來,即兩個線程合二為一,相當於一個線程(如果不執行插入的話,那目標線程和依賴線程就是並存執行),而且目標線程是插在主線程前面,所以目標線程先執行,但你主線程只需要等我5秒,5秒之後,不管我有沒有執行完畢,我們兩都分開,這時又會變成兩個並存執行的線程,而不是目標線程直接結束執行,這點很重要。
其實這個方法比較牽強,因為它主要作用是用來多個線程之間進行同步的。但因為它提供了這個帶參數的方法(所以這也給了我們一個更廣泛的思路,就是一般帶有逾時參數的方法我們都可以嘗試著用它來實現逾時結束任務),所以我們可以用它來實現。注意這裡的參數的單位是固定的毫秒,不同於接下來的帶單位的函數。具體用法請看樣本:
public class JoinTest { public static void main(String[] args) { Task task1 = new Task("one", 4); Task task2 = new Task("two", 2); Thread t1 = new Thread(task1); Thread t2 = new Thread(task2); t1.start(); try { t1.join(2000); // 在主線程中等待t1執行2秒 } catch (InterruptedException e) { System.out.println("t1 interrupted when waiting join"); e.printStackTrace(); } t1.interrupt(); // 這裡很重要,一定要打斷t1,因為它已經執行了2秒。 t2.start(); try { t2.join(1000); } catch (InterruptedException e) { System.out.println("t2 interrupted when waiting join"); e.printStackTrace(); } }}class Task implements Runnable { public String name; private int time; public Task(String s, int t) { name = s; time = t; } public void run() { for (int i = 0; i < time; ++i) { System.out.println("task " + name + " " + (i + 1) + " round"); try { Thread.sleep(1000); } catch (InterruptedException e) { System.out.println(name + "is interrupted when calculating, will stop..."); return; // 注意這裡如果不return的話,線程還會繼續執行,所以任務逾時後在這裡處理結果然後返回 } } }}
在主線程中等待t1執行2秒之後,要interrupt(而不是直接調用stop,這個方法已經被棄用)掉它,然後在t1裡面會產出一個中斷異常,在異常裡面處理完該處理的事,就要return,一定要return,如果不return的話,t1還會繼續執行,只不過是與主線程並存執行。 方法二:Future.get(long million, TimeUnit unit) 配合Future.cancle(true)
Future系列(它的子類)的都可以實現,這裡採用最簡單的Future介面實現。
public class FutureTest { static class Task implements Callable<Boolean> { public String name; private int time; public Task(String s, int t) { name = s; time = t; } @Override public Boolean call() throws Exception { for (int i = 0; i < time; ++i) { System.out.println("task " + name + " round " + (i + 1)); try { Thread.sleep(1000); } catch (InterruptedException e) { System.out.println(name + " is interrupted when calculating, will stop..."); return false; // 注意這裡如果不return的話,線程還會繼續執行,所以任務逾時後在這裡處理結果然後返回 } } return true; } } public static void main(String[] args) { ExecutorService executor = Executors.newCachedThreadPool(); Task task1 = new Task("one", 5); Future<Boolean> f1 = executor.submit(task1); try { if (f1.get(2, TimeUnit.SECONDS)) { // future將在2秒之後取結果 System.out.println("one complete successfully"); } } catch (InterruptedException e) { System.out.println("future在睡著時被打斷"); executor.shutdownNow(); } catch (ExecutionException e) { System.out.println("future在嘗試取得任務結果時出錯"); executor.shutdownNow(); } catch (TimeoutException e) { System.out.println("future時間逾時"); f1.cancel(true); // executor.shutdownNow(); // executor.shutdown(); } finally { executor.shutdownNow(); } }}
運行結果如下,task在2秒之後停止:
如果把Task中捕獲InterruptedException的catch塊中的return注釋掉,就是這樣的結果:
task繼續執行,直至結束 方法三:ExecutorService.awaitTermination(long million, TimeUnit unit)
這個方法會一直等待所有的任務都結束,或者逾時時間到立即返回,若所有任務都完成則返回true,否則返回false
public class AwaitTermination { static class Task implements Runnable { public String name; private int time; public Task(String s, int t) { name = s; time = t; } public void run() { for (int i = 0; i < time; ++i) { try { Thread.sleep(1000); } catch (InterruptedException e) { System.out.println(name + " is interrupted when calculating, will stop..."); return; // 注意這裡如果不return的話,線程還會繼續執行,所以任務逾時後在這裡處理結果然後返回 } System.out.println("task " + name + " " + (i + 1) + " round"); } System.out.println("task " + name + " finished successfully"); } } public static void main(String[] args) { ExecutorService executor = Executors.newCachedThreadPool(); Task task = new Task("one", 5); Task task2 = new Task("two", 2); Future<?> future = executor.submit(task); Future<?> future2 = executor.submit(task2); List<Future<?>> futures = new ArrayList<Future<?>>(); futures.add(future); futures.add(future2); try { if (executor.awaitTermination(3, TimeUnit.SECONDS)) { System.out.println("task finished"); } else { System.out.println("task time out,will terminate"); for (Future<?> f : futures) { if (!f.isDone()) { f.cancel(true); } } } } catch (InterruptedException e) { System.out.println("executor is interrupted"); } finally { executor.shutdown(); } }}
運行結果如下:
方法四:設定一個守護線程,守護線程先sleep一段定時時間,睡醒後打斷它所監視的線程
public class DemonThread { static class Task implements Runnable { private String name; private int time; public Task(String s, int t) { name = s; time = t; } public int getTime() { return time; } public void run() { for (int i = 0; i < time; ++i) { try { Thread.sleep(1000); } catch (InterruptedException e) { System.out.println(name + " is interrupted when calculating, will stop..."); return; // 注意這裡如果不return的話,線程還會繼續執行,所以任務逾時後在這裡處理結果然後返回 } System.out.println("task " + name + " " + (i + 1) + " round"); } System.out.println("task " + name + " finished successfully"); } } static class Daemon implements Runnable { List<Runnable> tasks = new ArrayList<Runnable>(); private Thread thread; private int time; public Daemon(Thread r, int t) { thread = r; time = t; } public void addTask(Runnable r) { tasks.add(r); } @Override public void run() { while (true) { try { Thread.sleep(time * 1000); } catch (InterruptedException e) { e.printStackTrace(); } thread.interrupt(); } } } public static void main(String[] args) { Task task1 = new Task("one", 5); Thread t1 = new Thread(task1); Daemon daemon = new Daemon(t1, 3); Thread daemoThread = new Thread(daemon); daemoThread.setDaemon(true); t1.start(); daemoThread.start(); }}
一開始準備在守護任務裡面用一個集合來實現監視多個任務,接著發現要實現這個功能還得在這個守護任務裡面為每一個監視的任務開啟一個監視任務,一時又想不到更好的方法來解決,索性只監視一個算了,留待以後改進吧。
運行結果如下:
方法五:使用Timer / TimerTask,或其他schedule定時相關的類
總結:需要注意的是,無論以上哪一種方法,其實現原理都是在逾時後通過interrupt打斷目標線程的運行,所以都要在捕捉到InterruptedException的catch代碼塊中return,否則線程仍然會繼續執行。另外,最後兩種方法本質上是一樣的,都是通過持有目標線程的引用,在定時結束後打斷目標線程,這兩種方法的控制精度最低,因為它是採用另一個線程來監視目標線程的已耗用時間,因為線程調度的不確定性,另一個線程在定時結束後不一定會馬上得到執行而打斷目標線程。