計劃寫一個"Java並發基礎實踐"系列,算作本人對Java並發學習與實踐的簡單總結。本文是該系列的第一篇,介紹了退出並發任務的最簡單方法。
在一個並發任務被啟動之後,不要期望它總是會執行完成。由於時間限制,資源限制,用戶操作,甚至是任務中的異常(尤其是運行時異常),...都可能造成任務不能執行完成。如何恰當地退出任務是一個很常見的問題,而且實現方法也不一而足。
1. 任務
創建一個並發任務,遞歸地獲取指定目錄下的所有子目錄與文件的絕對路徑,最後再將這些路徑信息保存到一個文件中,如代碼清單1所示:
清單1 public class FileScanner implements Runnable { private File root = null; private List<String> filePaths = new ArrayList<String>(); public FileScanner1(File root) { if (root == null || !root.exists() || !root.isDirectory()) { throw new IllegalArgumentException("root must be legal directory"); } this.root = root; } @Override public void run() { travleFiles(root); try { saveFilePaths(); } catch (Exception e) { e.printStackTrace(); } } private void travleFiles(File parent) { String filePath = parent.getAbsolutePath(); filePaths.add(filePath); if (parent.isDirectory()) { File[] children = parent.listFiles(); for (File child : children) { travleFiles(child); } } } private void saveFilePaths() throws IOException { FileWriter fos = new FileWriter(new File(root.getAbsoluteFile() + File.separator + "filePaths.out")); for (String filePath : filePaths) { fos.write(filePath + "\n"); } fos.close(); } }
2. 停止線程
有一個很直接,也很干脆的方式來停止線程,就是調用Thread.stop()方法,如代碼清單2所示:
清單2 public static void main(String[] args) throws Exception { FileScanner task = new FileScanner(new File("C:")); Thread taskThread = new Thread(task); taskThread.start(); TimeUnit.SECONDS.sleep(1); taskThread.stop(); }
但是,地球人都知道Thread.stop()在很久很久之前就不推薦使用了。根據官方文檔的介紹,該方法存在著固有的不安全性。當停止線程時,將會釋放該線程所占有的全部監視鎖,這就會造成受這些鎖保護的對象的不一致性。在執行清單2的應用程序時,它的運行結果是不確定的。它可能會輸出一個文件,其中包含部分的被掃描過的目錄和文件。但它也很有可能什麼也不輸出,因為在執行FileWriter.write()的過程中,可能由於線程停止而造成了I/O異常,使得最終無法得到輸出文件。
3. 可取消的任務
另外一種十分常見的途徑是,在設計之初,我們就使任務是可被取消的。一般地,就是提供一個取消標志或設定一個取消條件,一旦任務遇到該標志或滿足了取消條件,就會結束任務的執行。如代碼清單3所示:
清單3 public class FileScanner implements Runnable { private File root = null; private List<String> filePaths = new ArrayList<String>(); private boolean cancel = false; public FileScanner(File root) { } @Override public void run() { } private void travleFiles(File parent) { if (cancel) { return; } String filePath = parent.getAbsolutePath(); filePaths.add(filePath); if (parent.isDirectory()) { File[] children = parent.listFiles(); for (File child : children) { travleFiles(child); } } } private void saveFilePaths() throws IOException { } public void cancel() { cancel = true; } }
新的FileScanner實現提供一個cancel標志,travleFiles()會遍歷新的文件之前檢測該標志,若該標志為true,則會立即返回。代碼清單4是使用新任務的應用程序。
清單4 public static void main(String[] args) throws Exception { FileScanner task = new FileScanner(new File("C:")); Thread taskThread = new Thread(task); taskThread.start(); TimeUnit.SECONDS.sleep(3); task.cancel(); }
查看本欄目
但有些時候使用可取消的任務,並不能快速地退出任務。因為任務在檢測取消標志之前,可能正處於等待狀態,甚至可能被阻塞著。對清單2中的FileScanner稍作修改,讓每次訪問新的文件之前先睡眠10秒鐘,如代碼清單5所示:
清單5 public class FileScanner implements Runnable { private void travleFiles(File parent) { try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } if (cancel) { return; } } private void saveFilePaths() throws IOException { } public void cancel() { cancel = true; } }
再執行清單3中的應用程序時,可能發現任務並沒有很快速的退出,而是又等待了大約7秒鐘才退出。如果在檢查cancel標志之前要先獲取某個受鎖保護的資源,那麼該任務就會被阻塞,並且無法確定何時能夠退出。對於這種情況,就需要使用中斷了。
4. 中斷
中斷是一種協作機制,它並不會真正地停止一個線程,而只是提醒線程需要被中斷,並將線程的中斷狀態設置為true。如果線程正在執行一些可拋出InterruptedException的方法,如Thread.sleep(),Thread.join()和Object.wait(),那麼當線程被中斷時,上述方法就會拋出InterruptedException,並且中斷狀態會被重新設置為false。任務程序只要恰當處理該異常,就可以正常地退出任務。對清單5再稍作修改,即,如果任務在睡眠時遇上了InterruptedException,那麼就取消任務。如代碼清單6所示:
清單6 public class FileScanner implements Runnable { private void travleFiles(File parent) { try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { cancel(); } if (cancel) { return; } } }
同時將清單4中的應用程序,此時將調用Thread.interrupt()方法去中斷線程,如代碼清單7所示:
清單7 public static void main(String[] args) throws Exception { FileScanner3 task = new FileScanner3(new File("C:")); Thread taskThread = new Thread(task); taskThread.start(); TimeUnit.SECONDS.sleep(3); taskThread.interrupt(); }
或者更進一步,僅使用中斷狀態來控制程序的退出,而不再使用可取消的任務(即,刪除cancel標志),將清單6中的FileScanner修改成如下:
清單8
public class FileScanner implements Runnable { private void travleFiles(File parent) { try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } if (Thread.currentThread().isInterrupted()) { return; } } }
再次執行清單7的應用程序後,新的FileScanner也能即時的退出了。值得注意的是,因為當sleep()方法拋出InterruptedException時,該線程的中斷狀態將又會被設置為false,所以必須要再次調用interrupt()方法來保存中斷狀態,這樣在後面才可以利用中斷狀態來判定是否需要返回travleFiles()方法。當然,對於此處的例子,在收到InterruptedException時也可以選擇直接返回,如代碼清單9所示:
清單9 public class FileScanner implements Runnable { private void travleFiles(File parent) { try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { return; } } }
5 小結
本文介紹了三種簡單的退出並發任務的方法:停止線程;使用可取消任務;使用中斷。毫無疑問,停止線程是不可取的。使用可取消的任務時,要避免任務由於被阻塞而無法及時,甚至永遠無法被取消。一般地,恰當地使用中斷是取消任務的首選方式。